···11use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
12#[cfg(all(target_family = "wasm", target_os = "unknown"))]
13use gloo_storage::{LocalStorage, Storage};
0014use loro::cursor::Cursor;
15use serde::{Deserialize, Serialize};
16···67/// # Arguments
68/// * `doc` - The editor document to save
69/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing)
70-/// * `editing_uri` - AT-URI if editing an existing entry
71#[cfg(all(target_family = "wasm", target_os = "unknown"))]
72pub fn save_to_storage(
73 doc: &EditorDocument,
74 key: &str,
75- editing_uri: Option<&str>,
76) -> Result<(), gloo_storage::errors::StorageError> {
77 let snapshot_bytes = doc.export_snapshot();
78 let snapshot_b64 = if snapshot_bytes.is_empty() {
···87 snapshot: snapshot_b64,
88 cursor: doc.loro_cursor().cloned(),
89 cursor_offset: doc.cursor.offset,
90- editing_uri: editing_uri.map(String::from),
91 };
92 LocalStorage::set(storage_key(key), &snapshot)
93}
···103pub fn load_from_storage(key: &str) -> Option<EditorDocument> {
104 let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?;
1050000000106 // Try to restore from CRDT snapshot first
107 if let Some(ref snapshot_b64) = snapshot.snapshot {
108 if let Ok(snapshot_bytes) = BASE64.decode(snapshot_b64) {
109- let doc = EditorDocument::from_snapshot(
110 &snapshot_bytes,
111 snapshot.cursor.clone(),
112 snapshot.cursor_offset,
113 );
114 // Verify the content matches (sanity check)
115 if doc.content() == snapshot.content {
0116 return Some(doc);
117 }
118 tracing::warn!("Snapshot content mismatch, falling back to text content");
···123 let mut doc = EditorDocument::new(snapshot.content);
124 doc.cursor.offset = snapshot.cursor_offset.min(doc.len_chars());
125 doc.sync_loro_cursor();
0126 Some(doc)
127}
128···176177// Stub implementations for non-WASM targets
178#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
179-pub fn save_to_storage(
180- _doc: &EditorDocument,
181- _key: &str,
182- _editing_uri: Option<&str>,
183-) -> Result<(), String> {
184 Ok(())
185}
186
···11use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
12#[cfg(all(target_family = "wasm", target_os = "unknown"))]
13use gloo_storage::{LocalStorage, Storage};
14+use jacquard::IntoStatic;
15+use jacquard::types::string::AtUri;
16use loro::cursor::Cursor;
17use serde::{Deserialize, Serialize};
18···69/// # Arguments
70/// * `doc` - The editor document to save
71/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing)
072#[cfg(all(target_family = "wasm", target_os = "unknown"))]
73pub fn save_to_storage(
74 doc: &EditorDocument,
75 key: &str,
076) -> Result<(), gloo_storage::errors::StorageError> {
77 let snapshot_bytes = doc.export_snapshot();
78 let snapshot_b64 = if snapshot_bytes.is_empty() {
···87 snapshot: snapshot_b64,
88 cursor: doc.loro_cursor().cloned(),
89 cursor_offset: doc.cursor.offset,
90+ editing_uri: doc.entry_uri().map(|u| u.to_string()),
91 };
92 LocalStorage::set(storage_key(key), &snapshot)
93}
···103pub fn load_from_storage(key: &str) -> Option<EditorDocument> {
104 let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?;
105106+ // Parse entry_uri from the snapshot
107+ let entry_uri = snapshot
108+ .editing_uri
109+ .as_ref()
110+ .and_then(|s| AtUri::new(s).ok())
111+ .map(|u| u.into_static());
112+113 // Try to restore from CRDT snapshot first
114 if let Some(ref snapshot_b64) = snapshot.snapshot {
115 if let Ok(snapshot_bytes) = BASE64.decode(snapshot_b64) {
116+ let mut doc = EditorDocument::from_snapshot(
117 &snapshot_bytes,
118 snapshot.cursor.clone(),
119 snapshot.cursor_offset,
120 );
121 // Verify the content matches (sanity check)
122 if doc.content() == snapshot.content {
123+ doc.set_entry_uri(entry_uri);
124 return Some(doc);
125 }
126 tracing::warn!("Snapshot content mismatch, falling back to text content");
···131 let mut doc = EditorDocument::new(snapshot.content);
132 doc.cursor.offset = snapshot.cursor_offset.min(doc.len_chars());
133 doc.sync_loro_cursor();
134+ doc.set_entry_uri(entry_uri);
135 Some(doc)
136}
137···185186// Stub implementations for non-WASM targets
187#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
188+pub fn save_to_storage(_doc: &EditorDocument, _key: &str) -> Result<(), String> {
0000189 Ok(())
190}
191
+7-7
crates/weaver-app/src/components/editor/tests.rs
···57 let doc = LoroDoc::new();
58 let text = doc.get_text("content");
59 text.insert(0, input).unwrap();
60- let (paragraphs, _cache) = render_paragraphs_incremental(&text, None, None);
61 paragraphs.iter().map(TestParagraph::from).collect()
62}
63···645646 // Initial state: "#" is a valid empty heading
647 text.insert(0, "#").unwrap();
648- let (paras1, cache1) = render_paragraphs_incremental(&text, None, None);
649650 eprintln!("State 1 ('#'): {}", paras1[0].html);
651 assert!(paras1[0].html.contains("<h1"), "# alone should be heading");
···656657 // Transition: add "t" to make "#t" - no longer a heading
658 text.insert(1, "t").unwrap();
659- let (paras2, _cache2) = render_paragraphs_incremental(&text, Some(&cache1), None);
660661 eprintln!("State 2 ('#t'): {}", paras2[0].html);
662 assert!(
···765 let doc = LoroDoc::new();
766 let text = doc.get_text("content");
767 text.insert(0, input).unwrap();
768- let (paragraphs, _cache) = render_paragraphs_incremental(&text, None, None);
769770 // With standard \n\n break, we expect 2 paragraphs (no gap element)
771 // Paragraph ranges include some trailing whitespace from markdown parsing
···794 let doc = LoroDoc::new();
795 let text = doc.get_text("content");
796 text.insert(0, input).unwrap();
797- let (paragraphs, _cache) = render_paragraphs_incremental(&text, None, None);
798799 // With extra newlines, we expect 3 elements: para, gap, para
800 assert_eq!(
···894 let text = doc.get_text("content");
895 text.insert(0, input).unwrap();
896897- let (paras1, cache1) = render_paragraphs_incremental(&text, None, None);
898 assert!(!cache1.paragraphs.is_empty(), "Cache should be populated");
899900 // Second render with same content should reuse cache
901- let (paras2, _cache2) = render_paragraphs_incremental(&text, Some(&cache1), None);
902903 // Should produce identical output
904 assert_eq!(paras1.len(), paras2.len());
···57 let doc = LoroDoc::new();
58 let text = doc.get_text("content");
59 text.insert(0, input).unwrap();
60+ let (paragraphs, _cache) = render_paragraphs_incremental(&text, None, None, None);
61 paragraphs.iter().map(TestParagraph::from).collect()
62}
63···645646 // Initial state: "#" is a valid empty heading
647 text.insert(0, "#").unwrap();
648+ let (paras1, cache1) = render_paragraphs_incremental(&text, None, None, None);
649650 eprintln!("State 1 ('#'): {}", paras1[0].html);
651 assert!(paras1[0].html.contains("<h1"), "# alone should be heading");
···656657 // Transition: add "t" to make "#t" - no longer a heading
658 text.insert(1, "t").unwrap();
659+ let (paras2, _cache2) = render_paragraphs_incremental(&text, Some(&cache1), None, None);
660661 eprintln!("State 2 ('#t'): {}", paras2[0].html);
662 assert!(
···765 let doc = LoroDoc::new();
766 let text = doc.get_text("content");
767 text.insert(0, input).unwrap();
768+ let (paragraphs, _cache) = render_paragraphs_incremental(&text, None, None, None);
769770 // With standard \n\n break, we expect 2 paragraphs (no gap element)
771 // Paragraph ranges include some trailing whitespace from markdown parsing
···794 let doc = LoroDoc::new();
795 let text = doc.get_text("content");
796 text.insert(0, input).unwrap();
797+ let (paragraphs, _cache) = render_paragraphs_incremental(&text, None, None, None);
798799 // With extra newlines, we expect 3 elements: para, gap, para
800 assert_eq!(
···894 let text = doc.get_text("content");
895 text.insert(0, input).unwrap();
896897+ let (paras1, cache1) = render_paragraphs_incremental(&text, None, None, None);
898 assert!(!cache1.paragraphs.is_empty(), "Cache should be populated");
899900 // Second render with same content should reuse cache
901+ let (paras2, _cache2) = render_paragraphs_incremental(&text, Some(&cache1), None, None);
902903 // Should produce identical output
904 assert_eq!(paras1.len(), paras2.len());