···141 tags.push(tag_str).ok();
142 }
143 }
00000000000000000000000000000000144 doc.commit();
145146 return LoadResult::Loaded(LoadedDocState {
···233 let auth_state = use_context::<Signal<AuthState>>();
234235 let mut document = use_hook(|| {
236- let doc = EditorDocument::from_loaded_state(loaded_state.clone());
0000000000000000237 storage::save_to_storage(&doc, &draft_key).ok();
238 doc
239 });
240 let editor_id = "markdown-editor";
241 let mut render_cache = use_signal(|| render::RenderCache::default());
242- let mut image_resolver = use_signal(EditorImageResolver::default);
00000000000243 let resolved_content = use_signal(weaver_common::ResolvedContent::default);
244245 let doc_for_memo = document.clone();
···1095 } else {
1096 uploaded.alt.clone()
1097 };
1098- let markdown = format!("", alt_text, name);
000000000000000010991100 let pos = doc.cursor.read().offset;
1101 let _ = doc.insert_tracked(pos, &markdown);
1102 doc.cursor.write().offset = pos + markdown.chars().count();
11031104 // Upload to PDS in background if authenticated
1105- let is_authenticated = auth_state.read().is_authenticated();
1106 if is_authenticated {
1107 let fetcher = fetcher.clone();
1108 let name_for_upload = name.clone();
···1116 // Clone data for cache pre-warming
1117 let data_for_cache = data.clone();
111800001119 // Upload blob and create temporary PublishedBlob record
1120- match client.publish_blob(data, &name_for_upload, None).await {
1121 Ok((strong_ref, published_blob)) => {
1122 // Get DID from fetcher
1123 let did = match fetcher.current_did().await {
···141 tags.push(tag_str).ok();
142 }
143 }
144+145+ // Restore existing embeds from the entry
146+ if let Some(ref embeds) = loaded.entry.embeds {
147+ let embeds_map = doc.get_map("embeds");
148+149+ // Restore images
150+ if let Some(ref images) = embeds.images {
151+ let images_list = embeds_map
152+ .get_or_create_container("images", loro::LoroList::new())
153+ .expect("images list");
154+ for image in &images.images {
155+ // Serialize image to JSON and add to list
156+ // No publishedBlobUri since these are already published
157+ let json = serde_json::to_value(image)
158+ .expect("Image serializes");
159+ images_list.push(json).ok();
160+ }
161+ }
162+163+ // Restore record embeds
164+ if let Some(ref records) = embeds.records {
165+ let records_list = embeds_map
166+ .get_or_create_container("records", loro::LoroList::new())
167+ .expect("records list");
168+ for record in &records.records {
169+ let json = serde_json::to_value(record)
170+ .expect("RecordEmbed serializes");
171+ records_list.push(json).ok();
172+ }
173+ }
174+ }
175+176 doc.commit();
177178 return LoadResult::Loaded(LoadedDocState {
···265 let auth_state = use_context::<Signal<AuthState>>();
266267 let mut document = use_hook(|| {
268+ let mut doc = EditorDocument::from_loaded_state(loaded_state.clone());
269+270+ // Seed collected_refs with existing record embeds so they get fetched/rendered
271+ let record_embeds = doc.record_embeds();
272+ if !record_embeds.is_empty() {
273+ let refs: Vec<weaver_common::ExtractedRef> = record_embeds
274+ .into_iter()
275+ .filter_map(|embed| {
276+ embed.name.map(|name| weaver_common::ExtractedRef::AtEmbed {
277+ uri: name.to_string(),
278+ alt_text: None,
279+ })
280+ })
281+ .collect();
282+ doc.set_collected_refs(refs);
283+ }
284+285 storage::save_to_storage(&doc, &draft_key).ok();
286 doc
287 });
288 let editor_id = "markdown-editor";
289 let mut render_cache = use_signal(|| render::RenderCache::default());
290+291+ // Populate resolver from existing images if editing a published entry
292+ let mut image_resolver: Signal<EditorImageResolver> = use_signal(|| {
293+ let images = document.images();
294+ if let (false, Some(ref r)) = (images.is_empty(), document.entry_ref()) {
295+ let ident = r.uri.authority().clone().into_static();
296+ let entry_rkey = r.uri.rkey().map(|rk| rk.0.clone().into_static());
297+ EditorImageResolver::from_images(&images, ident, entry_rkey)
298+ } else {
299+ EditorImageResolver::default()
300+ }
301+ });
302 let resolved_content = use_signal(weaver_common::ResolvedContent::default);
303304 let doc_for_memo = document.clone();
···1154 } else {
1155 uploaded.alt.clone()
1156 };
1157+1158+ // Check if authenticated and get DID for draft path
1159+ let auth = auth_state.read();
1160+ let did_for_path = auth.did.clone();
1161+ let is_authenticated = auth.is_authenticated();
1162+ drop(auth);
1163+1164+ // Pre-generate TID for the blob rkey (used in draft path and upload)
1165+ let blob_tid = jacquard::types::tid::Ticker::new().next(None);
1166+1167+ // Build markdown with proper draft path if authenticated
1168+ let markdown = if let Some(ref did) = did_for_path {
1169+ format!("", alt_text, did, blob_tid.as_str(), name)
1170+ } else {
1171+ // Fallback for unauthenticated - simple path (won't be publishable anyway)
1172+ format!("", alt_text, name)
1173+ };
11741175 let pos = doc.cursor.read().offset;
1176 let _ = doc.insert_tracked(pos, &markdown);
1177 doc.cursor.write().offset = pos + markdown.chars().count();
11781179 // Upload to PDS in background if authenticated
01180 if is_authenticated {
1181 let fetcher = fetcher.clone();
1182 let name_for_upload = name.clone();
···1190 // Clone data for cache pre-warming
1191 let data_for_cache = data.clone();
11921193+ // Use pre-generated TID as rkey for the blob record
1194+ let rkey = jacquard::types::recordkey::RecordKey::any(blob_tid.as_str())
1195+ .expect("TID is valid record key");
1196+1197 // Upload blob and create temporary PublishedBlob record
1198+ match client.publish_blob(data, &name_for_upload, Some(rkey)).await {
1199 Ok((strong_ref, published_blob)) => {
1200 // Get DID from fetcher
1201 let did = match fetcher.current_did().await {
···26use jacquard::types::string::AtUri;
27use weaver_api::com_atproto::repo::strong_ref::StrongRef;
28use weaver_api::sh_weaver::embed::images::Image;
029use weaver_api::sh_weaver::notebook::entry::Entry;
3031/// Helper for working with editor images.
···612 }
613 }
614 }
0000000000000000000000000000000615 }
616617 /// Insert text into content and record edit info for incremental rendering.
···26use jacquard::types::string::AtUri;
27use weaver_api::com_atproto::repo::strong_ref::StrongRef;
28use weaver_api::sh_weaver::embed::images::Image;
29+use weaver_api::sh_weaver::embed::records::RecordEmbed;
30use weaver_api::sh_weaver::notebook::entry::Entry;
3132/// Helper for working with editor images.
···613 }
614 }
615 }
616+ }
617+618+ // --- Record embed methods ---
619+620+ /// Get the records LoroList from embeds, creating it if needed.
621+ fn get_records_list(&self) -> LoroList {
622+ self.embeds
623+ .get_or_create_container("records", LoroList::new())
624+ .unwrap()
625+ }
626+627+ /// Get all record embeds as a Vec.
628+ pub fn record_embeds(&self) -> Vec<RecordEmbed<'static>> {
629+ let records_list = self.get_records_list();
630+ let mut result = Vec::new();
631+632+ for i in 0..records_list.len() {
633+ if let Some(record_embed) = self.loro_value_to_record_embed(&records_list, i) {
634+ result.push(record_embed);
635+ }
636+ }
637+638+ result
639+ }
640+641+ /// Convert a LoroValue at the given index to a RecordEmbed.
642+ fn loro_value_to_record_embed(&self, list: &LoroList, index: usize) -> Option<RecordEmbed<'static>> {
643+ let value = list.get(index)?;
644+ let loro_value = value.as_value()?;
645+ let json = loro_value.to_json_value();
646+ from_json_value::<RecordEmbed>(json).ok().map(|r| r.into_static())
647 }
648649 /// Insert text into content and record edit info for incremental rendering.
···206 blob_rkey: Rkey<'static>,
207 ident: AtIdentifier<'static>,
208 ) {
209- self.images.insert(
210- name.to_string(),
211- ResolvedImage::Draft { blob_rkey, ident },
212- );
213 }
214215 /// Add an already-uploaded draft image.
···314///
315/// This writer processes offset-iter events to detect gaps (consumed formatting)
316/// and emits them as styled spans for visibility in the editor.
317-pub struct EditorWriter<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, W: StrWrite, E = (), R = ()> {
000000318 source: &'a str,
319 source_text: &'a LoroText,
320 events: I,
···387}
388389impl<
390- 'a,
391- I: Iterator<Item = (Event<'a>, Range<usize>)>,
392- W: StrWrite,
393- E: EmbedContentProvider,
394- R: ImageResolver,
395- > EditorWriter<'a, I, W, E, R>
396{
397 pub fn new(source: &'a str, source_text: &'a LoroText, events: I, writer: W) -> Self {
398 Self::new_with_node_offset(source, source_text, events, writer, 0)
···1329 write!(
1330 &mut self.writer,
1331 "<span class=\"math math-inline math-rendered math-clickable\" contenteditable=\"false\" data-char-target=\"{}\">{}</span>",
1332- content_char_start,
1333- mathml
1334 )?;
1335 }
1336 weaver_renderer::math::MathResult::Error { html, .. } => {
···1423 write!(
1424 &mut self.writer,
1425 "<span class=\"math math-display math-rendered math-clickable\" contenteditable=\"false\" data-char-target=\"{}\">{}</span>",
1426- content_char_start,
1427- mathml
1428 )?;
1429 }
1430 weaver_renderer::math::MathResult::Error { html, .. } => {
···1740 }
17411742 // Emit the opening tag
1743- tracing::debug!(?tag, "start_tag");
1744 match tag {
1745 Tag::HtmlBlock => Ok(()),
1746 Tag::Paragraph => {
···2612 Err(_) => {
2613 // Fallback to plain code block
2614 if let Some(ref nid) = node_id {
2615- write!(&mut self.writer, "<pre data-node-id=\"{}\"><code class=\"language-", nid)?;
00002616 } else {
2617 self.write("<pre><code class=\"language-")?;
2618 }
···2666 self.last_byte_offset += fence.len();
26672668 // Compute formatted_range for entire code block (opening fence to closing fence)
2669- let formatted_range = code_block_start
2670- .map(|start| start..self.last_char_offset);
26712672 // Update opening fence span with formatted_range
2673- if let (Some(idx), Some(fr)) = (opening_span_idx, formatted_range.as_ref())
02674 {
2675 if let Some(span) = self.syntax_spans.get_mut(idx) {
2676 span.formatted_range = Some(fr.clone());
···2809}
28102811impl<
2812- 'a,
2813- I: Iterator<Item = (Event<'a>, Range<usize>)>,
2814- W: StrWrite,
2815- E: EmbedContentProvider,
2816- R: ImageResolver,
2817- > EditorWriter<'a, I, W, E, R>
2818{
2819 fn write_embed(
2820 &mut self,
···2891 formatted_range: Some(formatted_range.clone()),
2892 });
28932894- self.record_mapping(
2895- range.start + 3..range.end - 2,
2896- url_char_start..url_char_end,
2897- );
28982899 // 3. Emit closing ]] syntax span
2900 if raw_text.ends_with("]]") {
···29192920 // Collect AT URI for later resolution
2921 if url.starts_with("at://") || url.starts_with("did:") {
2922- self.ref_collector.add_at_embed(url, if title.is_empty() { None } else { Some(title.as_ref()) });
00000002923 }
29242925 // 4. Emit the actual embed content
···206 blob_rkey: Rkey<'static>,
207 ident: AtIdentifier<'static>,
208 ) {
209+ self.images
210+ .insert(name.to_string(), ResolvedImage::Draft { blob_rkey, ident });
00211 }
212213 /// Add an already-uploaded draft image.
···312///
313/// This writer processes offset-iter events to detect gaps (consumed formatting)
314/// and emits them as styled spans for visibility in the editor.
315+pub struct EditorWriter<
316+ 'a,
317+ I: Iterator<Item = (Event<'a>, Range<usize>)>,
318+ W: StrWrite,
319+ E = (),
320+ R = (),
321+> {
322 source: &'a str,
323 source_text: &'a LoroText,
324 events: I,
···391}
392393impl<
394+ 'a,
395+ I: Iterator<Item = (Event<'a>, Range<usize>)>,
396+ W: StrWrite,
397+ E: EmbedContentProvider,
398+ R: ImageResolver,
399+> EditorWriter<'a, I, W, E, R>
400{
401 pub fn new(source: &'a str, source_text: &'a LoroText, events: I, writer: W) -> Self {
402 Self::new_with_node_offset(source, source_text, events, writer, 0)
···1333 write!(
1334 &mut self.writer,
1335 "<span class=\"math math-inline math-rendered math-clickable\" contenteditable=\"false\" data-char-target=\"{}\">{}</span>",
1336+ content_char_start, mathml
01337 )?;
1338 }
1339 weaver_renderer::math::MathResult::Error { html, .. } => {
···1426 write!(
1427 &mut self.writer,
1428 "<span class=\"math math-display math-rendered math-clickable\" contenteditable=\"false\" data-char-target=\"{}\">{}</span>",
1429+ content_char_start, mathml
01430 )?;
1431 }
1432 weaver_renderer::math::MathResult::Error { html, .. } => {
···1742 }
17431744 // Emit the opening tag
01745 match tag {
1746 Tag::HtmlBlock => Ok(()),
1747 Tag::Paragraph => {
···2613 Err(_) => {
2614 // Fallback to plain code block
2615 if let Some(ref nid) = node_id {
2616+ write!(
2617+ &mut self.writer,
2618+ "<pre data-node-id=\"{}\"><code class=\"language-",
2619+ nid
2620+ )?;
2621 } else {
2622 self.write("<pre><code class=\"language-")?;
2623 }
···2671 self.last_byte_offset += fence.len();
26722673 // Compute formatted_range for entire code block (opening fence to closing fence)
2674+ let formatted_range =
2675+ code_block_start.map(|start| start..self.last_char_offset);
26762677 // Update opening fence span with formatted_range
2678+ if let (Some(idx), Some(fr)) =
2679+ (opening_span_idx, formatted_range.as_ref())
2680 {
2681 if let Some(span) = self.syntax_spans.get_mut(idx) {
2682 span.formatted_range = Some(fr.clone());
···2815}
28162817impl<
2818+ 'a,
2819+ I: Iterator<Item = (Event<'a>, Range<usize>)>,
2820+ W: StrWrite,
2821+ E: EmbedContentProvider,
2822+ R: ImageResolver,
2823+> EditorWriter<'a, I, W, E, R>
2824{
2825 fn write_embed(
2826 &mut self,
···2897 formatted_range: Some(formatted_range.clone()),
2898 });
28992900+ self.record_mapping(range.start + 3..range.end - 2, url_char_start..url_char_end);
00029012902 // 3. Emit closing ]] syntax span
2903 if raw_text.ends_with("]]") {
···29222923 // Collect AT URI for later resolution
2924 if url.starts_with("at://") || url.starts_with("did:") {
2925+ self.ref_collector.add_at_embed(
2926+ url,
2927+ if title.is_empty() {
2928+ None
2929+ } else {
2930+ Some(title.as_ref())
2931+ },
2932+ );
2933 }
29342935 // 4. Emit the actual embed content
+6-2
crates/weaver-app/src/components/entry.rs
···835836/// Render some text as markdown.
837pub fn EntryMarkdown(props: EntryMarkdownProps) -> Element {
838- let processed = crate::data::use_rendered_markdown(props.content, props.ident);
00839840 match &*processed.read() {
841 Some(html_buf) => rsx! {
···866 // Use feature-gated hook for SSR support
867 let content = use_signal(|| content);
868 let ident = use_signal(|| ident);
869- let processed = crate::data::use_rendered_markdown(content.into(), ident.into());
00870871 match &*processed.read() {
872 Some(html_buf) => rsx! {
···835836/// Render some text as markdown.
837pub fn EntryMarkdown(props: EntryMarkdownProps) -> Element {
838+ let (_res, processed) = crate::data::use_rendered_markdown(props.content, props.ident);
839+ #[cfg(feature = "fullstack-server")]
840+ _res?;
841842 match &*processed.read() {
843 Some(html_buf) => rsx! {
···868 // Use feature-gated hook for SSR support
869 let content = use_signal(|| content);
870 let ident = use_signal(|| ident);
871+ let (_res, processed) = crate::data::use_rendered_markdown(content.into(), ident.into());
872+ #[cfg(feature = "fullstack-server")]
873+ _res?;
874875 match &*processed.read() {
876 Some(html_buf) => rsx! {
+20-19
crates/weaver-app/src/components/identity.rs
···4647#[component]
48pub fn Repository(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
49- tracing::debug!("Repository component rendering for ident: {:?}", ident());
50- // Fetch notebooks for this specific DID with SSR support;
51- tracing::debug!("Repository component context set up");
52-53 rsx! {
54 div {
55 Outlet::<Route> {}
···5960#[component]
61pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
62- tracing::debug!(
63- "RepositoryIndex component rendering for ident: {:?}",
64- ident()
65- );
66 use crate::components::ProfileDisplay;
67 let (notebooks_result, notebooks) = data::use_notebooks_for_did(ident);
68 let (profile_result, profile) = crate::data::use_profile_data(ident);
69- tracing::debug!("RepositoryIndex got profile and notebooks");
7071 #[cfg(feature = "fullstack-server")]
72 notebooks_result?;
···8182 let (display_name, handle, bio) = match &profile_view.inner {
83 ProfileDataViewInner::ProfileView(p) => (
84- p.display_name.as_ref().map(|n| n.as_ref().to_string()).unwrap_or_default(),
00085 p.handle.as_ref().to_string(),
86- p.description.as_ref().map(|d| d.as_ref().to_string()).unwrap_or_default(),
00087 ),
88 ProfileDataViewInner::ProfileViewDetailed(p) => (
89- p.display_name.as_ref().map(|n| n.as_ref().to_string()).unwrap_or_default(),
90- p.handle.as_ref().to_string(),
91- p.description.as_ref().map(|d| d.as_ref().to_string()).unwrap_or_default(),
92- ),
93- ProfileDataViewInner::TangledProfileView(p) => (
94- String::new(),
95 p.handle.as_ref().to_string(),
96- String::new(),
00097 ),
00098 _ => (String::new(), "unknown".to_string(), String::new()),
99 };
100···174 notebook: NotebookView<'static>,
175 entry_refs: Vec<StrongRef<'static>>,
176) -> Element {
177- use jacquard::{from_data, IntoStatic};
178 use weaver_api::sh_weaver::notebook::book::Book;
179180 let fetcher = use_context::<fetch::Fetcher>();