1use mdcs_core::lattice::Lattice;
28use mdcs_db::{JsonCrdt, JsonPath, JsonValue, MarkType, RGAText, RichText};
29use serde::{Deserialize, Serialize};
30use wasm_bindgen::prelude::*;
31
32#[wasm_bindgen(start)]
34pub fn init_panic_hook() {
35 #[cfg(feature = "console_error_panic_hook")]
36 console_error_panic_hook::set_once();
37}
38
39#[wasm_bindgen]
48pub struct CollaborativeDocument {
49 id: String,
50 replica_id: String,
51 text: RichText,
52 version: u64,
53}
54
55#[wasm_bindgen]
56impl CollaborativeDocument {
57 #[wasm_bindgen(constructor)]
63 pub fn new(doc_id: &str, replica_id: &str) -> Self {
64 Self {
65 id: doc_id.to_string(),
66 replica_id: replica_id.to_string(),
67 text: RichText::new(replica_id),
68 version: 0,
69 }
70 }
71
72 #[wasm_bindgen]
78 pub fn insert(&mut self, position: usize, text: &str) {
79 let pos = position.min(self.text.len());
80 self.text.insert(pos, text);
81 self.version += 1;
82 }
83
84 #[wasm_bindgen]
90 pub fn delete(&mut self, position: usize, length: usize) {
91 let pos = position.min(self.text.len());
92 let len = length.min(self.text.len().saturating_sub(pos));
93 if len > 0 {
94 self.text.delete(pos, len);
95 self.version += 1;
96 }
97 }
98
99 #[wasm_bindgen]
105 pub fn apply_bold(&mut self, start: usize, end: usize) {
106 self.apply_mark(start, end, MarkType::Bold);
107 }
108
109 #[wasm_bindgen]
111 pub fn apply_italic(&mut self, start: usize, end: usize) {
112 self.apply_mark(start, end, MarkType::Italic);
113 }
114
115 #[wasm_bindgen]
117 pub fn apply_underline(&mut self, start: usize, end: usize) {
118 self.apply_mark(start, end, MarkType::Underline);
119 }
120
121 #[wasm_bindgen]
123 pub fn apply_strikethrough(&mut self, start: usize, end: usize) {
124 self.apply_mark(start, end, MarkType::Strikethrough);
125 }
126
127 #[wasm_bindgen]
129 pub fn apply_code(&mut self, start: usize, end: usize) {
130 self.apply_mark(start, end, MarkType::Code);
131 }
132
133 #[wasm_bindgen]
140 pub fn apply_link(&mut self, start: usize, end: usize, url: &str) {
141 let s = start.min(self.text.len());
142 let e = end.min(self.text.len());
143 if s < e {
144 self.text.add_mark(
145 s,
146 e,
147 MarkType::Link {
148 url: url.to_string(),
149 },
150 );
151 self.version += 1;
152 }
153 }
154
155 #[wasm_bindgen]
162 pub fn apply_highlight(&mut self, start: usize, end: usize, color: &str) {
163 let s = start.min(self.text.len());
164 let e = end.min(self.text.len());
165 if s < e {
166 self.text.add_mark(
167 s,
168 e,
169 MarkType::Highlight {
170 color: color.to_string(),
171 },
172 );
173 self.version += 1;
174 }
175 }
176
177 #[wasm_bindgen]
185 pub fn apply_comment(&mut self, start: usize, end: usize, author: &str, content: &str) {
186 let s = start.min(self.text.len());
187 let e = end.min(self.text.len());
188 if s < e {
189 self.text.add_mark(
190 s,
191 e,
192 MarkType::Comment {
193 author: author.to_string(),
194 content: content.to_string(),
195 },
196 );
197 self.version += 1;
198 }
199 }
200
201 #[wasm_bindgen]
209 pub fn apply_custom_mark(&mut self, start: usize, end: usize, name: &str, value: &str) {
210 let s = start.min(self.text.len());
211 let e = end.min(self.text.len());
212 if s < e {
213 self.text.add_mark(
214 s,
215 e,
216 MarkType::Custom {
217 name: name.to_string(),
218 value: value.to_string(),
219 },
220 );
221 self.version += 1;
222 }
223 }
224
225 #[wasm_bindgen]
227 pub fn get_text(&self) -> String {
228 self.text.to_string()
229 }
230
231 #[wasm_bindgen]
233 pub fn get_html(&self) -> String {
234 self.text.to_html()
235 }
236
237 #[wasm_bindgen]
239 pub fn len(&self) -> usize {
240 self.text.len()
241 }
242
243 #[wasm_bindgen]
245 pub fn is_empty(&self) -> bool {
246 self.text.len() == 0
247 }
248
249 #[wasm_bindgen]
254 pub fn version(&self) -> u64 {
255 self.version
256 }
257
258 #[wasm_bindgen]
260 pub fn doc_id(&self) -> String {
261 self.id.clone()
262 }
263
264 #[wasm_bindgen]
266 pub fn replica_id(&self) -> String {
267 self.replica_id.clone()
268 }
269
270 #[wasm_bindgen]
275 pub fn serialize(&self) -> Result<String, JsValue> {
276 let js_value = serde_wasm_bindgen::to_value(&self.text)
278 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
279
280 js_sys::JSON::stringify(&js_value)
282 .map(|s| s.into())
283 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))
284 }
285
286 #[wasm_bindgen]
294 pub fn merge(&mut self, remote_state: &str) -> Result<(), JsValue> {
295 let js_value = js_sys::JSON::parse(remote_state)
297 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
298
299 let remote: RichText = serde_wasm_bindgen::from_value(js_value)
301 .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
302
303 self.text = self.text.join(&remote);
304 self.version += 1;
305 Ok(())
306 }
307
308 #[wasm_bindgen]
312 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
313 let state_js = serde_wasm_bindgen::to_value(&self.text)
314 .map_err(|e| JsValue::from_str(&e.to_string()))?;
315 let state_str: String = js_sys::JSON::stringify(&state_js)
316 .map(|s| s.into())
317 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))?;
318
319 let snapshot = DocumentSnapshot {
320 doc_id: self.id.clone(),
321 replica_id: self.replica_id.clone(),
322 version: self.version,
323 state: state_str,
324 };
325 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
326 }
327
328 #[wasm_bindgen]
330 pub fn restore(snapshot_js: JsValue) -> Result<CollaborativeDocument, JsValue> {
331 let snapshot: DocumentSnapshot = serde_wasm_bindgen::from_value(snapshot_js)
332 .map_err(|e| JsValue::from_str(&e.to_string()))?;
333
334 let state_js = js_sys::JSON::parse(&snapshot.state)
336 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
337
338 let text: RichText = serde_wasm_bindgen::from_value(state_js)
339 .map_err(|e| JsValue::from_str(&e.to_string()))?;
340
341 Ok(Self {
342 id: snapshot.doc_id,
343 replica_id: snapshot.replica_id,
344 text,
345 version: snapshot.version,
346 })
347 }
348
349 fn apply_mark(&mut self, start: usize, end: usize, mark: MarkType) {
351 let s = start.min(self.text.len());
352 let e = end.min(self.text.len());
353 if s < e {
354 self.text.add_mark(s, e, mark);
355 self.version += 1;
356 }
357 }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362struct DocumentSnapshot {
363 doc_id: String,
364 replica_id: String,
365 version: u64,
366 state: String,
367}
368
369#[wasm_bindgen]
375pub struct TextDocument {
376 id: String,
377 replica_id: String,
378 text: RGAText,
379 version: u64,
380}
381
382#[wasm_bindgen]
383impl TextDocument {
384 #[wasm_bindgen(constructor)]
385 pub fn new(doc_id: &str, replica_id: &str) -> Self {
386 Self {
387 id: doc_id.to_string(),
388 replica_id: replica_id.to_string(),
389 text: RGAText::new(replica_id),
390 version: 0,
391 }
392 }
393
394 #[wasm_bindgen]
395 pub fn insert(&mut self, position: usize, text: &str) {
396 let pos = position.min(self.text.len());
397 self.text.insert(pos, text);
398 self.version += 1;
399 }
400
401 #[wasm_bindgen]
402 pub fn delete(&mut self, position: usize, length: usize) {
403 let pos = position.min(self.text.len());
404 let len = length.min(self.text.len().saturating_sub(pos));
405 if len > 0 {
406 self.text.delete(pos, len);
407 self.version += 1;
408 }
409 }
410
411 #[wasm_bindgen]
412 pub fn replace(&mut self, start: usize, end: usize, text: &str) {
413 let s = start.min(self.text.len());
414 let e = end.min(self.text.len());
415 if s <= e {
416 self.text.replace(s, e, text);
417 self.version += 1;
418 }
419 }
420
421 #[wasm_bindgen]
422 pub fn splice(&mut self, position: usize, delete_count: usize, insert: &str) {
423 let pos = position.min(self.text.len());
424 self.text.splice(pos, delete_count, insert);
425 self.version += 1;
426 }
427
428 #[wasm_bindgen]
429 pub fn get_text(&self) -> String {
430 self.text.to_string()
431 }
432
433 #[wasm_bindgen]
434 pub fn len(&self) -> usize {
435 self.text.len()
436 }
437
438 #[wasm_bindgen]
439 pub fn is_empty(&self) -> bool {
440 self.text.is_empty()
441 }
442
443 #[wasm_bindgen]
444 pub fn version(&self) -> u64 {
445 self.version
446 }
447
448 #[wasm_bindgen]
449 pub fn doc_id(&self) -> String {
450 self.id.clone()
451 }
452
453 #[wasm_bindgen]
454 pub fn replica_id(&self) -> String {
455 self.replica_id.clone()
456 }
457
458 #[wasm_bindgen]
459 pub fn serialize(&self) -> Result<String, JsValue> {
460 let js_value = serde_wasm_bindgen::to_value(&self.text)
461 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
462
463 js_sys::JSON::stringify(&js_value)
464 .map(|s| s.into())
465 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))
466 }
467
468 #[wasm_bindgen]
469 pub fn merge(&mut self, remote_state: &str) -> Result<(), JsValue> {
470 let js_value = js_sys::JSON::parse(remote_state)
471 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
472
473 let remote: RGAText = serde_wasm_bindgen::from_value(js_value)
474 .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
475
476 self.text = self.text.join(&remote);
477 self.version += 1;
478 Ok(())
479 }
480
481 #[wasm_bindgen]
482 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
483 let state_js = serde_wasm_bindgen::to_value(&self.text)
484 .map_err(|e| JsValue::from_str(&e.to_string()))?;
485 let state_str: String = js_sys::JSON::stringify(&state_js)
486 .map(|s| s.into())
487 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))?;
488
489 let snapshot = DocumentSnapshot {
490 doc_id: self.id.clone(),
491 replica_id: self.replica_id.clone(),
492 version: self.version,
493 state: state_str,
494 };
495 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
496 }
497
498 #[wasm_bindgen]
499 pub fn restore(snapshot_js: JsValue) -> Result<TextDocument, JsValue> {
500 let snapshot: DocumentSnapshot = serde_wasm_bindgen::from_value(snapshot_js)
501 .map_err(|e| JsValue::from_str(&e.to_string()))?;
502
503 let state_js = js_sys::JSON::parse(&snapshot.state)
504 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
505
506 let text: RGAText =
507 serde_wasm_bindgen::from_value(state_js).map_err(|e| JsValue::from_str(&e.to_string()))?;
508
509 Ok(Self {
510 id: snapshot.doc_id,
511 replica_id: snapshot.replica_id,
512 text,
513 version: snapshot.version,
514 })
515 }
516}
517
518#[wasm_bindgen]
526pub struct RichTextDocument {
527 inner: CollaborativeDocument,
528}
529
530#[wasm_bindgen]
531impl RichTextDocument {
532 #[wasm_bindgen(constructor)]
533 pub fn new(doc_id: &str, replica_id: &str) -> Self {
534 Self {
535 inner: CollaborativeDocument::new(doc_id, replica_id),
536 }
537 }
538
539 #[wasm_bindgen]
540 pub fn insert(&mut self, position: usize, text: &str) {
541 self.inner.insert(position, text);
542 }
543
544 #[wasm_bindgen]
545 pub fn delete(&mut self, position: usize, length: usize) {
546 self.inner.delete(position, length);
547 }
548
549 #[wasm_bindgen]
550 pub fn apply_bold(&mut self, start: usize, end: usize) {
551 self.inner.apply_bold(start, end);
552 }
553
554 #[wasm_bindgen]
555 pub fn apply_italic(&mut self, start: usize, end: usize) {
556 self.inner.apply_italic(start, end);
557 }
558
559 #[wasm_bindgen]
560 pub fn apply_underline(&mut self, start: usize, end: usize) {
561 self.inner.apply_underline(start, end);
562 }
563
564 #[wasm_bindgen]
565 pub fn apply_strikethrough(&mut self, start: usize, end: usize) {
566 self.inner.apply_strikethrough(start, end);
567 }
568
569 #[wasm_bindgen]
570 pub fn apply_code(&mut self, start: usize, end: usize) {
571 self.inner.apply_code(start, end);
572 }
573
574 #[wasm_bindgen]
575 pub fn apply_link(&mut self, start: usize, end: usize, url: &str) {
576 self.inner.apply_link(start, end, url);
577 }
578
579 #[wasm_bindgen]
580 pub fn apply_highlight(&mut self, start: usize, end: usize, color: &str) {
581 self.inner.apply_highlight(start, end, color);
582 }
583
584 #[wasm_bindgen]
585 pub fn apply_comment(&mut self, start: usize, end: usize, author: &str, content: &str) {
586 self.inner.apply_comment(start, end, author, content);
587 }
588
589 #[wasm_bindgen]
590 pub fn apply_custom_mark(&mut self, start: usize, end: usize, name: &str, value: &str) {
591 self.inner.apply_custom_mark(start, end, name, value);
592 }
593
594 #[wasm_bindgen]
595 pub fn get_text(&self) -> String {
596 self.inner.get_text()
597 }
598
599 #[wasm_bindgen]
600 pub fn get_html(&self) -> String {
601 self.inner.get_html()
602 }
603
604 #[wasm_bindgen]
605 pub fn len(&self) -> usize {
606 self.inner.len()
607 }
608
609 #[wasm_bindgen]
610 pub fn is_empty(&self) -> bool {
611 self.inner.is_empty()
612 }
613
614 #[wasm_bindgen]
615 pub fn version(&self) -> u64 {
616 self.inner.version()
617 }
618
619 #[wasm_bindgen]
620 pub fn doc_id(&self) -> String {
621 self.inner.doc_id()
622 }
623
624 #[wasm_bindgen]
625 pub fn replica_id(&self) -> String {
626 self.inner.replica_id()
627 }
628
629 #[wasm_bindgen]
630 pub fn serialize(&self) -> Result<String, JsValue> {
631 self.inner.serialize()
632 }
633
634 #[wasm_bindgen]
635 pub fn merge(&mut self, remote_state: &str) -> Result<(), JsValue> {
636 self.inner.merge(remote_state)
637 }
638
639 #[wasm_bindgen]
640 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
641 self.inner.snapshot()
642 }
643
644 #[wasm_bindgen]
645 pub fn restore(snapshot_js: JsValue) -> Result<RichTextDocument, JsValue> {
646 Ok(Self {
647 inner: CollaborativeDocument::restore(snapshot_js)?,
648 })
649 }
650}
651
652#[wasm_bindgen]
658pub struct JsonDocument {
659 id: String,
660 replica_id: String,
661 doc: JsonCrdt,
662 version: u64,
663}
664
665#[wasm_bindgen]
666impl JsonDocument {
667 #[wasm_bindgen(constructor)]
668 pub fn new(doc_id: &str, replica_id: &str) -> Self {
669 Self {
670 id: doc_id.to_string(),
671 replica_id: replica_id.to_string(),
672 doc: JsonCrdt::new(replica_id),
673 version: 0,
674 }
675 }
676
677 #[wasm_bindgen]
678 pub fn set_string(&mut self, path: &str, value: &str) -> Result<(), JsValue> {
679 self.doc
680 .set(&JsonPath::parse(path), JsonValue::String(value.to_string()))
681 .map_err(|e| JsValue::from_str(&e.to_string()))?;
682 self.version += 1;
683 Ok(())
684 }
685
686 #[wasm_bindgen]
687 pub fn set_int(&mut self, path: &str, value: i64) -> Result<(), JsValue> {
688 self.doc
689 .set(&JsonPath::parse(path), JsonValue::Int(value))
690 .map_err(|e| JsValue::from_str(&e.to_string()))?;
691 self.version += 1;
692 Ok(())
693 }
694
695 #[wasm_bindgen]
696 pub fn set_float(&mut self, path: &str, value: f64) -> Result<(), JsValue> {
697 self.doc
698 .set(&JsonPath::parse(path), JsonValue::Float(value))
699 .map_err(|e| JsValue::from_str(&e.to_string()))?;
700 self.version += 1;
701 Ok(())
702 }
703
704 #[wasm_bindgen]
705 pub fn set_bool(&mut self, path: &str, value: bool) -> Result<(), JsValue> {
706 self.doc
707 .set(&JsonPath::parse(path), JsonValue::Bool(value))
708 .map_err(|e| JsValue::from_str(&e.to_string()))?;
709 self.version += 1;
710 Ok(())
711 }
712
713 #[wasm_bindgen]
714 pub fn set_null(&mut self, path: &str) -> Result<(), JsValue> {
715 self.doc
716 .set(&JsonPath::parse(path), JsonValue::Null)
717 .map_err(|e| JsValue::from_str(&e.to_string()))?;
718 self.version += 1;
719 Ok(())
720 }
721
722 #[wasm_bindgen]
723 pub fn set_object(&mut self, path: &str) -> Result<(), JsValue> {
724 self.doc
725 .set_object(&JsonPath::parse(path))
726 .map_err(|e| JsValue::from_str(&e.to_string()))?;
727 self.version += 1;
728 Ok(())
729 }
730
731 #[wasm_bindgen]
732 pub fn set_array(&mut self, path: &str) -> Result<(), JsValue> {
733 self.doc
734 .set_array(&JsonPath::parse(path))
735 .map_err(|e| JsValue::from_str(&e.to_string()))?;
736 self.version += 1;
737 Ok(())
738 }
739
740 #[wasm_bindgen]
741 pub fn array_push_string(&mut self, path: &str, value: &str) -> Result<(), JsValue> {
742 let arr_id = self.get_array_id(path)?;
743 self.doc
744 .array_push(&arr_id, JsonValue::String(value.to_string()))
745 .map_err(|e| JsValue::from_str(&e.to_string()))?;
746 self.version += 1;
747 Ok(())
748 }
749
750 #[wasm_bindgen]
751 pub fn array_push_int(&mut self, path: &str, value: i64) -> Result<(), JsValue> {
752 let arr_id = self.get_array_id(path)?;
753 self.doc
754 .array_push(&arr_id, JsonValue::Int(value))
755 .map_err(|e| JsValue::from_str(&e.to_string()))?;
756 self.version += 1;
757 Ok(())
758 }
759
760 #[wasm_bindgen]
761 pub fn array_push_float(&mut self, path: &str, value: f64) -> Result<(), JsValue> {
762 let arr_id = self.get_array_id(path)?;
763 self.doc
764 .array_push(&arr_id, JsonValue::Float(value))
765 .map_err(|e| JsValue::from_str(&e.to_string()))?;
766 self.version += 1;
767 Ok(())
768 }
769
770 #[wasm_bindgen]
771 pub fn array_push_bool(&mut self, path: &str, value: bool) -> Result<(), JsValue> {
772 let arr_id = self.get_array_id(path)?;
773 self.doc
774 .array_push(&arr_id, JsonValue::Bool(value))
775 .map_err(|e| JsValue::from_str(&e.to_string()))?;
776 self.version += 1;
777 Ok(())
778 }
779
780 #[wasm_bindgen]
781 pub fn array_push_null(&mut self, path: &str) -> Result<(), JsValue> {
782 let arr_id = self.get_array_id(path)?;
783 self.doc
784 .array_push(&arr_id, JsonValue::Null)
785 .map_err(|e| JsValue::from_str(&e.to_string()))?;
786 self.version += 1;
787 Ok(())
788 }
789
790 #[wasm_bindgen]
791 pub fn array_remove(&mut self, path: &str, index: usize) -> Result<JsValue, JsValue> {
792 let arr_id = self.get_array_id(path)?;
793 let removed = self
794 .doc
795 .array_remove(&arr_id, index)
796 .map_err(|e| JsValue::from_str(&e.to_string()))?;
797 self.version += 1;
798
799 let removed_json = match removed {
800 JsonValue::Null => serde_json::Value::Null,
801 JsonValue::Bool(b) => serde_json::Value::Bool(b),
802 JsonValue::Int(i) => serde_json::Value::Number(i.into()),
803 JsonValue::Float(f) => serde_json::Number::from_f64(f)
804 .map(serde_json::Value::Number)
805 .unwrap_or(serde_json::Value::Null),
806 JsonValue::String(s) => serde_json::Value::String(s),
807 JsonValue::Array(_) | JsonValue::Object(_) => serde_json::Value::String(
808 "[complex_json_reference]".to_string(),
809 ),
810 };
811
812 serde_wasm_bindgen::to_value(&removed_json).map_err(|e| JsValue::from_str(&e.to_string()))
813 }
814
815 #[wasm_bindgen]
816 pub fn delete(&mut self, path: &str) -> Result<(), JsValue> {
817 self.doc
818 .delete(&JsonPath::parse(path))
819 .map_err(|e| JsValue::from_str(&e.to_string()))?;
820 self.version += 1;
821 Ok(())
822 }
823
824 #[wasm_bindgen]
825 pub fn get(&self, path: &str) -> Result<JsValue, JsValue> {
826 let root = self.doc.to_json();
827 let maybe_value = get_json_at_dot_path(&root, path);
828 match maybe_value {
829 Some(value) => {
830 serde_wasm_bindgen::to_value(&value).map_err(|e| JsValue::from_str(&e.to_string()))
831 }
832 None => Ok(JsValue::UNDEFINED),
833 }
834 }
835
836 #[wasm_bindgen]
837 pub fn to_json(&self) -> Result<JsValue, JsValue> {
838 serde_wasm_bindgen::to_value(&self.doc.to_json())
839 .map_err(|e| JsValue::from_str(&e.to_string()))
840 }
841
842 #[wasm_bindgen]
843 pub fn keys(&self) -> Result<JsValue, JsValue> {
844 let keys = self.doc.keys();
845 serde_wasm_bindgen::to_value(&keys).map_err(|e| JsValue::from_str(&e.to_string()))
846 }
847
848 #[wasm_bindgen]
849 pub fn contains_key(&self, key: &str) -> bool {
850 self.doc.contains_key(key)
851 }
852
853 #[wasm_bindgen]
854 pub fn version(&self) -> u64 {
855 self.version
856 }
857
858 #[wasm_bindgen]
859 pub fn doc_id(&self) -> String {
860 self.id.clone()
861 }
862
863 #[wasm_bindgen]
864 pub fn replica_id(&self) -> String {
865 self.replica_id.clone()
866 }
867
868 #[wasm_bindgen]
869 pub fn serialize(&self) -> Result<String, JsValue> {
870 let js_value = serde_wasm_bindgen::to_value(&self.doc)
871 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
872
873 js_sys::JSON::stringify(&js_value)
874 .map(|s| s.into())
875 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))
876 }
877
878 #[wasm_bindgen]
879 pub fn merge(&mut self, remote_state: &str) -> Result<(), JsValue> {
880 let js_value = js_sys::JSON::parse(remote_state)
881 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
882
883 let remote: JsonCrdt = serde_wasm_bindgen::from_value(js_value)
884 .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
885
886 self.doc = self.doc.join(&remote);
887 self.version += 1;
888 Ok(())
889 }
890
891 #[wasm_bindgen]
892 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
893 let state_js = serde_wasm_bindgen::to_value(&self.doc)
894 .map_err(|e| JsValue::from_str(&e.to_string()))?;
895 let state_str: String = js_sys::JSON::stringify(&state_js)
896 .map(|s| s.into())
897 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))?;
898
899 let snapshot = DocumentSnapshot {
900 doc_id: self.id.clone(),
901 replica_id: self.replica_id.clone(),
902 version: self.version,
903 state: state_str,
904 };
905 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
906 }
907
908 #[wasm_bindgen]
909 pub fn restore(snapshot_js: JsValue) -> Result<JsonDocument, JsValue> {
910 let snapshot: DocumentSnapshot = serde_wasm_bindgen::from_value(snapshot_js)
911 .map_err(|e| JsValue::from_str(&e.to_string()))?;
912
913 let state_js = js_sys::JSON::parse(&snapshot.state)
914 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
915
916 let doc: JsonCrdt =
917 serde_wasm_bindgen::from_value(state_js).map_err(|e| JsValue::from_str(&e.to_string()))?;
918
919 Ok(Self {
920 id: snapshot.doc_id,
921 replica_id: snapshot.replica_id,
922 doc,
923 version: snapshot.version,
924 })
925 }
926}
927
928impl JsonDocument {
929 fn get_array_id(&self, path: &str) -> Result<mdcs_db::json_crdt::ArrayId, JsValue> {
930 let json_path = JsonPath::parse(path);
931 let value = self
932 .doc
933 .get(&json_path)
934 .ok_or_else(|| JsValue::from_str(&format!("Path not found: {}", path)))?;
935
936 match value {
937 JsonValue::Array(id) => Ok(id.clone()),
938 _ => Err(JsValue::from_str(&format!(
939 "Path is not an array: {}",
940 path
941 ))),
942 }
943 }
944}
945
946fn get_json_at_dot_path(root: &serde_json::Value, path: &str) -> Option<serde_json::Value> {
947 if path.is_empty() {
948 return Some(root.clone());
949 }
950
951 let mut current = root;
952 for seg in path.split('.') {
953 if let Ok(idx) = seg.parse::<usize>() {
954 current = current.get(idx)?;
955 } else {
956 current = current.get(seg)?;
957 }
958 }
959
960 Some(current.clone())
961}
962
963#[wasm_bindgen]
972pub struct UserPresence {
973 user_id: String,
974 user_name: String,
975 color: String,
976 cursor_position: Option<usize>,
977 selection_start: Option<usize>,
978 selection_end: Option<usize>,
979}
980
981#[wasm_bindgen]
982impl UserPresence {
983 #[wasm_bindgen(constructor)]
990 pub fn new(user_id: &str, user_name: &str, color: &str) -> Self {
991 Self {
992 user_id: user_id.to_string(),
993 user_name: user_name.to_string(),
994 color: color.to_string(),
995 cursor_position: None,
996 selection_start: None,
997 selection_end: None,
998 }
999 }
1000
1001 #[wasm_bindgen]
1003 pub fn set_cursor(&mut self, position: usize) {
1004 self.cursor_position = Some(position);
1005 self.selection_start = None;
1006 self.selection_end = None;
1007 }
1008
1009 #[wasm_bindgen]
1011 pub fn set_selection(&mut self, start: usize, end: usize) {
1012 self.cursor_position = Some(end);
1013 self.selection_start = Some(start.min(end));
1014 self.selection_end = Some(start.max(end));
1015 }
1016
1017 #[wasm_bindgen]
1019 pub fn clear(&mut self) {
1020 self.cursor_position = None;
1021 self.selection_start = None;
1022 self.selection_end = None;
1023 }
1024
1025 #[wasm_bindgen(getter)]
1027 pub fn user_id(&self) -> String {
1028 self.user_id.clone()
1029 }
1030
1031 #[wasm_bindgen(getter)]
1033 pub fn user_name(&self) -> String {
1034 self.user_name.clone()
1035 }
1036
1037 #[wasm_bindgen(getter)]
1039 pub fn color(&self) -> String {
1040 self.color.clone()
1041 }
1042
1043 #[wasm_bindgen(getter)]
1045 pub fn cursor(&self) -> Option<usize> {
1046 self.cursor_position
1047 }
1048
1049 #[wasm_bindgen(getter)]
1051 pub fn selection_start(&self) -> Option<usize> {
1052 self.selection_start
1053 }
1054
1055 #[wasm_bindgen(getter)]
1057 pub fn selection_end(&self) -> Option<usize> {
1058 self.selection_end
1059 }
1060
1061 #[wasm_bindgen]
1063 pub fn has_selection(&self) -> bool {
1064 self.selection_start.is_some() && self.selection_end.is_some()
1065 }
1066
1067 #[wasm_bindgen]
1069 pub fn to_json(&self) -> Result<JsValue, JsValue> {
1070 let data = PresenceData {
1071 user_id: self.user_id.clone(),
1072 user_name: self.user_name.clone(),
1073 color: self.color.clone(),
1074 cursor: self.cursor_position,
1075 selection_start: self.selection_start,
1076 selection_end: self.selection_end,
1077 };
1078 serde_wasm_bindgen::to_value(&data).map_err(|e| JsValue::from_str(&e.to_string()))
1079 }
1080
1081 #[wasm_bindgen]
1083 pub fn from_json(js: JsValue) -> Result<UserPresence, JsValue> {
1084 let data: PresenceData =
1085 serde_wasm_bindgen::from_value(js).map_err(|e| JsValue::from_str(&e.to_string()))?;
1086
1087 Ok(Self {
1088 user_id: data.user_id,
1089 user_name: data.user_name,
1090 color: data.color,
1091 cursor_position: data.cursor,
1092 selection_start: data.selection_start,
1093 selection_end: data.selection_end,
1094 })
1095 }
1096}
1097
1098#[derive(Debug, Clone, Serialize, Deserialize)]
1099struct PresenceData {
1100 user_id: String,
1101 user_name: String,
1102 color: String,
1103 cursor: Option<usize>,
1104 selection_start: Option<usize>,
1105 selection_end: Option<usize>,
1106}
1107
1108#[wasm_bindgen]
1116pub fn generate_replica_id() -> String {
1117 let timestamp = js_sys::Date::now() as u64;
1118 let random: u32 = js_sys::Math::random().to_bits() as u32;
1119 format!("{}-{:x}", timestamp, random)
1120}
1121
1122#[wasm_bindgen]
1124pub fn generate_user_color() -> String {
1125 let colors = [
1126 "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F",
1127 "#E74C3C", "#3498DB", "#2ECC71", "#9B59B6", "#1ABC9C", "#F39C12", "#E91E63", "#00BCD4",
1128 ];
1129 let idx = (js_sys::Math::random() * colors.len() as f64) as usize;
1130 colors[idx % colors.len()].to_string()
1131}
1132
1133#[wasm_bindgen]
1135pub fn console_log(message: &str) {
1136 web_sys::console::log_1(&JsValue::from_str(message));
1137}
1138
1139#[cfg(test)]
1144mod tests {
1145 use super::*;
1146
1147 #[test]
1148 fn test_document_creation() {
1149 let doc = CollaborativeDocument::new("doc-1", "replica-1");
1150 assert_eq!(doc.doc_id(), "doc-1");
1151 assert_eq!(doc.replica_id(), "replica-1");
1152 assert_eq!(doc.len(), 0);
1153 assert!(doc.is_empty());
1154 }
1155
1156 #[test]
1157 fn test_insert_and_delete() {
1158 let mut doc = CollaborativeDocument::new("doc-1", "replica-1");
1159
1160 doc.insert(0, "Hello, World!");
1161 assert_eq!(doc.get_text(), "Hello, World!");
1162 assert_eq!(doc.len(), 13);
1163
1164 doc.delete(5, 2); assert_eq!(doc.get_text(), "HelloWorld!");
1166 }
1167
1168 #[test]
1169 fn test_formatting() {
1170 let mut doc = CollaborativeDocument::new("doc-1", "replica-1");
1171
1172 doc.insert(0, "Hello World");
1173 doc.apply_bold(0, 5);
1174 doc.apply_italic(6, 11);
1175
1176 let html = doc.get_html();
1177 assert!(html.contains("<b>") || html.contains("<strong>"));
1178 assert!(html.contains("<i>") || html.contains("<em>"));
1179 }
1180
1181 #[test]
1186 fn test_crdt_merge_convergence() {
1187 let mut doc1 = CollaborativeDocument::new("doc-1", "replica-1");
1189 let mut doc2 = CollaborativeDocument::new("doc-1", "replica-2");
1190
1191 doc1.insert(0, "Hello");
1192 doc2.insert(0, "World");
1193
1194 let text1_clone = doc1.text.clone();
1196 let text2_clone = doc2.text.clone();
1197
1198 doc1.text = doc1.text.join(&text2_clone);
1199 doc2.text = doc2.text.join(&text1_clone);
1200
1201 assert_eq!(doc1.get_text(), doc2.get_text());
1203 let final_text = doc1.get_text();
1205 assert!(final_text.contains("Hello") || final_text.contains("World"));
1206 }
1207
1208 #[test]
1209 fn test_user_presence() {
1210 let mut presence = UserPresence::new("user-1", "Alice", "#FF6B6B");
1211
1212 assert_eq!(presence.user_id(), "user-1");
1213 assert_eq!(presence.user_name(), "Alice");
1214 assert!(!presence.has_selection());
1215
1216 presence.set_cursor(10);
1217 assert_eq!(presence.cursor(), Some(10));
1218 assert!(!presence.has_selection());
1219
1220 presence.set_selection(5, 15);
1221 assert!(presence.has_selection());
1222 assert_eq!(presence.selection_start(), Some(5));
1223 assert_eq!(presence.selection_end(), Some(15));
1224 }
1225
1226 #[test]
1227 fn test_extended_mark_types() {
1228 let mut doc = CollaborativeDocument::new("doc-1", "replica-1");
1229 doc.insert(0, "hello world");
1230
1231 let initial_version = doc.version();
1232
1233 doc.apply_code(0, 5);
1234 doc.apply_highlight(6, 11, "#FFEAA7");
1235 doc.apply_comment(0, 11, "alice", "review this");
1236 doc.apply_custom_mark(0, 5, "tag", "important");
1237
1238 assert!(doc.version() >= initial_version + 4);
1239 assert_eq!(doc.get_text(), "hello world");
1240 assert_eq!(doc.len(), 11);
1241 }
1242
1243 #[test]
1244 fn test_text_document_api() {
1245 let mut doc = TextDocument::new("text-doc", "replica-1");
1246 doc.insert(0, "Hello");
1247 doc.insert(5, " World");
1248 assert_eq!(doc.get_text(), "Hello World");
1249
1250 doc.replace(6, 11, "Rust");
1251 assert_eq!(doc.get_text(), "Hello Rust");
1252
1253 doc.splice(5, 1, ",");
1254 assert_eq!(doc.get_text(), "Hello,Rust");
1255 assert!(doc.version() > 0);
1256 }
1257
1258 #[test]
1259 fn test_rich_text_document_wrapper() {
1260 let mut doc = RichTextDocument::new("rich-doc", "replica-1");
1261 doc.insert(0, "hello world");
1262 doc.apply_bold(0, 5);
1263 doc.apply_code(6, 11);
1264
1265 assert_eq!(doc.get_text(), "hello world");
1266 assert_eq!(doc.len(), 11);
1267 assert!(doc.version() > 0);
1268 }
1269
1270 #[test]
1271 fn test_json_document_api() {
1272 let mut doc = JsonDocument::new("json-doc", "replica-1");
1273 doc.set_string("name", "Alice").unwrap();
1274 doc.set_int("age", 30).unwrap();
1275 doc.set_bool("active", true).unwrap();
1276 doc.set_object("profile").unwrap();
1277 doc.set_string("profile.city", "Chennai").unwrap();
1278 doc.set_array("tags").unwrap();
1279 doc.array_push_string("tags", "crdt").unwrap();
1280 doc.array_push_string("tags", "wasm").unwrap();
1281
1282 let root_v = doc.doc.to_json();
1283 assert_eq!(root_v["name"], "Alice");
1284 assert_eq!(root_v["profile"]["city"], "Chennai");
1285 assert_eq!(root_v["tags"][0], "crdt");
1286 assert!(doc.version() > 0);
1287 }
1288}