1use mdcs_core::lattice::Lattice;
28use mdcs_db::{MarkType, 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]
134 pub fn apply_link(&mut self, start: usize, end: usize, url: &str) {
135 let s = start.min(self.text.len());
136 let e = end.min(self.text.len());
137 if s < e {
138 self.text.add_mark(
139 s,
140 e,
141 MarkType::Link {
142 url: url.to_string(),
143 },
144 );
145 self.version += 1;
146 }
147 }
148
149 #[wasm_bindgen]
151 pub fn get_text(&self) -> String {
152 self.text.to_string()
153 }
154
155 #[wasm_bindgen]
157 pub fn get_html(&self) -> String {
158 self.text.to_html()
159 }
160
161 #[wasm_bindgen]
163 pub fn len(&self) -> usize {
164 self.text.len()
165 }
166
167 #[wasm_bindgen]
169 pub fn is_empty(&self) -> bool {
170 self.text.len() == 0
171 }
172
173 #[wasm_bindgen]
178 pub fn version(&self) -> u64 {
179 self.version
180 }
181
182 #[wasm_bindgen]
184 pub fn doc_id(&self) -> String {
185 self.id.clone()
186 }
187
188 #[wasm_bindgen]
190 pub fn replica_id(&self) -> String {
191 self.replica_id.clone()
192 }
193
194 #[wasm_bindgen]
199 pub fn serialize(&self) -> Result<String, JsValue> {
200 let js_value = serde_wasm_bindgen::to_value(&self.text)
202 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
203
204 js_sys::JSON::stringify(&js_value)
206 .map(|s| s.into())
207 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))
208 }
209
210 #[wasm_bindgen]
218 pub fn merge(&mut self, remote_state: &str) -> Result<(), JsValue> {
219 let js_value = js_sys::JSON::parse(remote_state)
221 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
222
223 let remote: RichText = serde_wasm_bindgen::from_value(js_value)
225 .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
226
227 self.text = self.text.join(&remote);
228 self.version += 1;
229 Ok(())
230 }
231
232 #[wasm_bindgen]
236 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
237 let state_js = serde_wasm_bindgen::to_value(&self.text)
238 .map_err(|e| JsValue::from_str(&e.to_string()))?;
239 let state_str: String = js_sys::JSON::stringify(&state_js)
240 .map(|s| s.into())
241 .map_err(|e| JsValue::from_str(&format!("JSON stringify error: {:?}", e)))?;
242
243 let snapshot = DocumentSnapshot {
244 doc_id: self.id.clone(),
245 replica_id: self.replica_id.clone(),
246 version: self.version,
247 state: state_str,
248 };
249 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
250 }
251
252 #[wasm_bindgen]
254 pub fn restore(snapshot_js: JsValue) -> Result<CollaborativeDocument, JsValue> {
255 let snapshot: DocumentSnapshot = serde_wasm_bindgen::from_value(snapshot_js)
256 .map_err(|e| JsValue::from_str(&e.to_string()))?;
257
258 let state_js = js_sys::JSON::parse(&snapshot.state)
260 .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?;
261
262 let text: RichText = serde_wasm_bindgen::from_value(state_js)
263 .map_err(|e| JsValue::from_str(&e.to_string()))?;
264
265 Ok(Self {
266 id: snapshot.doc_id,
267 replica_id: snapshot.replica_id,
268 text,
269 version: snapshot.version,
270 })
271 }
272
273 fn apply_mark(&mut self, start: usize, end: usize, mark: MarkType) {
275 let s = start.min(self.text.len());
276 let e = end.min(self.text.len());
277 if s < e {
278 self.text.add_mark(s, e, mark);
279 self.version += 1;
280 }
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286struct DocumentSnapshot {
287 doc_id: String,
288 replica_id: String,
289 version: u64,
290 state: String,
291}
292
293#[wasm_bindgen]
302pub struct UserPresence {
303 user_id: String,
304 user_name: String,
305 color: String,
306 cursor_position: Option<usize>,
307 selection_start: Option<usize>,
308 selection_end: Option<usize>,
309}
310
311#[wasm_bindgen]
312impl UserPresence {
313 #[wasm_bindgen(constructor)]
320 pub fn new(user_id: &str, user_name: &str, color: &str) -> Self {
321 Self {
322 user_id: user_id.to_string(),
323 user_name: user_name.to_string(),
324 color: color.to_string(),
325 cursor_position: None,
326 selection_start: None,
327 selection_end: None,
328 }
329 }
330
331 #[wasm_bindgen]
333 pub fn set_cursor(&mut self, position: usize) {
334 self.cursor_position = Some(position);
335 self.selection_start = None;
336 self.selection_end = None;
337 }
338
339 #[wasm_bindgen]
341 pub fn set_selection(&mut self, start: usize, end: usize) {
342 self.cursor_position = Some(end);
343 self.selection_start = Some(start.min(end));
344 self.selection_end = Some(start.max(end));
345 }
346
347 #[wasm_bindgen]
349 pub fn clear(&mut self) {
350 self.cursor_position = None;
351 self.selection_start = None;
352 self.selection_end = None;
353 }
354
355 #[wasm_bindgen(getter)]
357 pub fn user_id(&self) -> String {
358 self.user_id.clone()
359 }
360
361 #[wasm_bindgen(getter)]
363 pub fn user_name(&self) -> String {
364 self.user_name.clone()
365 }
366
367 #[wasm_bindgen(getter)]
369 pub fn color(&self) -> String {
370 self.color.clone()
371 }
372
373 #[wasm_bindgen(getter)]
375 pub fn cursor(&self) -> Option<usize> {
376 self.cursor_position
377 }
378
379 #[wasm_bindgen(getter)]
381 pub fn selection_start(&self) -> Option<usize> {
382 self.selection_start
383 }
384
385 #[wasm_bindgen(getter)]
387 pub fn selection_end(&self) -> Option<usize> {
388 self.selection_end
389 }
390
391 #[wasm_bindgen]
393 pub fn has_selection(&self) -> bool {
394 self.selection_start.is_some() && self.selection_end.is_some()
395 }
396
397 #[wasm_bindgen]
399 pub fn to_json(&self) -> Result<JsValue, JsValue> {
400 let data = PresenceData {
401 user_id: self.user_id.clone(),
402 user_name: self.user_name.clone(),
403 color: self.color.clone(),
404 cursor: self.cursor_position,
405 selection_start: self.selection_start,
406 selection_end: self.selection_end,
407 };
408 serde_wasm_bindgen::to_value(&data).map_err(|e| JsValue::from_str(&e.to_string()))
409 }
410
411 #[wasm_bindgen]
413 pub fn from_json(js: JsValue) -> Result<UserPresence, JsValue> {
414 let data: PresenceData =
415 serde_wasm_bindgen::from_value(js).map_err(|e| JsValue::from_str(&e.to_string()))?;
416
417 Ok(Self {
418 user_id: data.user_id,
419 user_name: data.user_name,
420 color: data.color,
421 cursor_position: data.cursor,
422 selection_start: data.selection_start,
423 selection_end: data.selection_end,
424 })
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429struct PresenceData {
430 user_id: String,
431 user_name: String,
432 color: String,
433 cursor: Option<usize>,
434 selection_start: Option<usize>,
435 selection_end: Option<usize>,
436}
437
438#[wasm_bindgen]
446pub fn generate_replica_id() -> String {
447 let timestamp = js_sys::Date::now() as u64;
448 let random: u32 = js_sys::Math::random().to_bits() as u32;
449 format!("{}-{:x}", timestamp, random)
450}
451
452#[wasm_bindgen]
454pub fn generate_user_color() -> String {
455 let colors = [
456 "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F",
457 "#E74C3C", "#3498DB", "#2ECC71", "#9B59B6", "#1ABC9C", "#F39C12", "#E91E63", "#00BCD4",
458 ];
459 let idx = (js_sys::Math::random() * colors.len() as f64) as usize;
460 colors[idx % colors.len()].to_string()
461}
462
463#[wasm_bindgen]
465pub fn console_log(message: &str) {
466 web_sys::console::log_1(&JsValue::from_str(message));
467}
468
469#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_document_creation() {
479 let doc = CollaborativeDocument::new("doc-1", "replica-1");
480 assert_eq!(doc.doc_id(), "doc-1");
481 assert_eq!(doc.replica_id(), "replica-1");
482 assert_eq!(doc.len(), 0);
483 assert!(doc.is_empty());
484 }
485
486 #[test]
487 fn test_insert_and_delete() {
488 let mut doc = CollaborativeDocument::new("doc-1", "replica-1");
489
490 doc.insert(0, "Hello, World!");
491 assert_eq!(doc.get_text(), "Hello, World!");
492 assert_eq!(doc.len(), 13);
493
494 doc.delete(5, 2); assert_eq!(doc.get_text(), "HelloWorld!");
496 }
497
498 #[test]
499 fn test_formatting() {
500 let mut doc = CollaborativeDocument::new("doc-1", "replica-1");
501
502 doc.insert(0, "Hello World");
503 doc.apply_bold(0, 5);
504 doc.apply_italic(6, 11);
505
506 let html = doc.get_html();
507 assert!(html.contains("<b>") || html.contains("<strong>"));
508 assert!(html.contains("<i>") || html.contains("<em>"));
509 }
510
511 #[test]
516 fn test_crdt_merge_convergence() {
517 let mut doc1 = CollaborativeDocument::new("doc-1", "replica-1");
519 let mut doc2 = CollaborativeDocument::new("doc-1", "replica-2");
520
521 doc1.insert(0, "Hello");
522 doc2.insert(0, "World");
523
524 let text1_clone = doc1.text.clone();
526 let text2_clone = doc2.text.clone();
527
528 doc1.text = doc1.text.join(&text2_clone);
529 doc2.text = doc2.text.join(&text1_clone);
530
531 assert_eq!(doc1.get_text(), doc2.get_text());
533 let final_text = doc1.get_text();
535 assert!(final_text.contains("Hello") || final_text.contains("World"));
536 }
537
538 #[test]
539 fn test_user_presence() {
540 let mut presence = UserPresence::new("user-1", "Alice", "#FF6B6B");
541
542 assert_eq!(presence.user_id(), "user-1");
543 assert_eq!(presence.user_name(), "Alice");
544 assert!(!presence.has_selection());
545
546 presence.set_cursor(10);
547 assert_eq!(presence.cursor(), Some(10));
548 assert!(!presence.has_selection());
549
550 presence.set_selection(5, 15);
551 assert!(presence.has_selection());
552 assert_eq!(presence.selection_start(), Some(5));
553 assert_eq!(presence.selection_end(), Some(15));
554 }
555}