and now using the preliminary atproto-aware renderer.

Orual 974cc1e0 8e78c389

+111 -31
+1
Cargo.lock
··· 8624 8624 "jacquard-axum", 8625 8625 "markdown-weaver", 8626 8626 "mini-moka", 8627 + "n0-future", 8627 8628 "time", 8628 8629 "weaver-api", 8629 8630 "weaver-common",
+20 -6
crates/weaver-renderer/src/atproto/client.rs
··· 115 115 } 116 116 } 117 117 118 + impl EmbedResolver for () { 119 + async fn resolve_profile(&self, uri: &AtUri<'_>) -> Result<String, ClientRenderError> { 120 + Ok("".to_string()) 121 + } 122 + 123 + async fn resolve_post(&self, uri: &AtUri<'_>) -> Result<String, ClientRenderError> { 124 + Ok("".to_string()) 125 + } 126 + 127 + async fn resolve_markdown(&self, url: &str, depth: usize) -> Result<String, ClientRenderError> { 128 + Ok("".to_string()) 129 + } 130 + } 131 + 118 132 const MAX_EMBED_DEPTH: usize = 3; 119 133 120 134 pub struct ClientContext<'a, R = ()> { ··· 134 148 title: MdCowStr<'a>, 135 149 } 136 150 137 - impl<'a> ClientContext<'a, ()> { 138 - pub fn new(entry: Entry<'a>, creator_did: Did<'a>) -> Self { 151 + impl<'a, R: EmbedResolver> ClientContext<'a, R> { 152 + pub fn new(entry: Entry<'a>, creator_did: Did<'a>) -> ClientContext<'a, ()> { 139 153 let blob_map = Self::build_blob_map(&entry); 140 154 let title = MdCowStr::Boxed(entry.title.as_ref().into()); 141 155 142 - Self { 156 + ClientContext { 143 157 entry, 144 158 creator_did, 145 159 blob_map, ··· 151 165 } 152 166 153 167 /// Add an embed resolver for fetching embed content 154 - pub fn with_embed_resolver<R: EmbedResolver>(self, resolver: Arc<R>) -> ClientContext<'a, R> { 168 + pub fn with_embed_resolver(self, resolver: Arc<R>) -> ClientContext<'a, R> { 155 169 ClientContext { 156 170 entry: self.entry, 157 171 creator_did: self.creator_did, ··· 164 178 } 165 179 } 166 180 167 - impl<'a, R> ClientContext<'a, R> { 181 + impl<'a, R: EmbedResolver> ClientContext<'a, R> { 168 182 /// Create a child context with incremented embed depth (for recursive embeds) 169 183 fn with_depth(&self, depth: usize) -> Self 170 184 where ··· 432 446 .created_at(Datetime::now()) 433 447 .build(); 434 448 435 - let ctx = ClientContext::new(entry, Did::new("did:plc:test").unwrap()); 449 + let ctx = ClientContext::<()>::new(entry, Did::new("did:plc:test").unwrap()); 436 450 assert_eq!(ctx.title.as_ref(), "Test"); 437 451 } 438 452
+42 -12
crates/weaver-renderer/src/atproto/writer.rs
··· 6 6 use markdown_weaver::{ 7 7 Alignment, BlockQuoteKind, CodeBlockKind, CowStr, EmbedType, Event, LinkType, Tag, 8 8 }; 9 - use markdown_weaver_escape::{escape_href, escape_html, escape_html_body_text, StrWrite}; 9 + use markdown_weaver_escape::{StrWrite, escape_href, escape_html, escape_html_body_text}; 10 10 use std::collections::HashMap; 11 11 12 12 /// Synchronous callback for injecting embed content ··· 16 16 fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String>; 17 17 } 18 18 19 + impl EmbedContentProvider for () { 20 + fn get_embed_content(&self, _tag: &Tag<'_>) -> Option<String> { 21 + None 22 + } 23 + } 24 + 19 25 /// Simple writer that outputs HTML from markdown events 20 26 /// 21 27 /// This writer is designed for client-side rendering where embeds may have ··· 40 46 Body, 41 47 } 42 48 43 - impl<W: StrWrite> ClientWriter<W, ()> { 49 + impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 44 50 pub fn new(writer: W) -> Self { 45 51 Self { 46 52 writer, ··· 55 61 } 56 62 57 63 /// Add an embed content provider 58 - pub fn with_embed_provider<E: EmbedContentProvider>(self, provider: E) -> ClientWriter<W, E> { 64 + pub fn with_embed_provider(self, provider: E) -> ClientWriter<W, E> { 59 65 ClientWriter { 60 66 writer: self.writer, 61 67 end_newline: self.end_newline, ··· 162 168 self.write("\n<p>") 163 169 } 164 170 } 165 - Tag::Heading { level, id, classes, attrs } => { 171 + Tag::Heading { 172 + level, 173 + id, 174 + classes, 175 + attrs, 176 + } => { 166 177 if !self.end_newline { 167 178 self.write("\n")?; 168 179 } ··· 314 325 Tag::Emphasis => self.write("<em>"), 315 326 Tag::Strong => self.write("<strong>"), 316 327 Tag::Strikethrough => self.write("<del>"), 317 - Tag::Link { link_type: LinkType::Email, dest_url, title, .. } => { 328 + Tag::Link { 329 + link_type: LinkType::Email, 330 + dest_url, 331 + title, 332 + .. 333 + } => { 318 334 self.write("<a href=\"mailto:")?; 319 335 escape_href(&mut self.writer, &dest_url)?; 320 336 if !title.is_empty() { ··· 323 339 } 324 340 self.write("\">") 325 341 } 326 - Tag::Link { dest_url, title, .. } => { 342 + Tag::Link { 343 + dest_url, title, .. 344 + } => { 327 345 self.write("<a href=\"")?; 328 346 escape_href(&mut self.writer, &dest_url)?; 329 347 if !title.is_empty() { ··· 332 350 } 333 351 self.write("\">") 334 352 } 335 - Tag::Image { dest_url, title, attrs, .. } => { 353 + Tag::Image { 354 + dest_url, 355 + title, 356 + attrs, 357 + .. 358 + } => { 336 359 self.write("<img src=\"")?; 337 360 escape_href(&mut self.writer, &dest_url)?; 338 361 self.write("\" alt=\"")?; ··· 362 385 } 363 386 self.write(" />") 364 387 } 365 - Tag::Embed { embed_type, dest_url, title, id, attrs } => { 366 - self.write_embed(embed_type, dest_url, title, id, attrs) 367 - } 388 + Tag::Embed { 389 + embed_type, 390 + dest_url, 391 + title, 392 + id, 393 + attrs, 394 + } => self.write_embed(embed_type, dest_url, title, id, attrs), 368 395 Tag::WeaverBlock(_, _) => { 369 396 self.in_non_writing_block = true; 370 397 Ok(()) ··· 441 468 } 442 469 } 443 470 } 471 + } 444 472 473 + impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 445 474 fn write_embed( 446 475 &mut self, 447 476 embed_type: EmbedType, ··· 452 481 ) -> Result<(), W::Error> { 453 482 // Try to get content from attributes first 454 483 let content_from_attrs = if let Some(ref attrs) = attrs { 455 - attrs.attrs.iter() 484 + attrs 485 + .attrs 486 + .iter() 456 487 .find(|(k, _)| k.as_ref() == "content") 457 488 .map(|(_, v)| v.as_ref().to_string()) 458 489 } else { ··· 515 546 } 516 547 self.write("></iframe>")?; 517 548 } 518 - 519 549 Ok(()) 520 550 } 521 551 }
+1
crates/weaver-server/Cargo.toml
··· 25 25 markdown-weaver = { workspace = true } 26 26 weaver-renderer = { path = "../weaver-renderer" } 27 27 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } 28 + n0-future = { workspace = true } 28 29 #dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } 29 30 axum = {version = "0.8.6", optional = true} 30 31
+47 -13
crates/weaver-server/src/components/entry.rs
··· 8 8 fullstack::{get_server_url, reqwest}, 9 9 prelude::*, 10 10 }; 11 - use jacquard::smol_str::ToSmolStr; 11 + use jacquard::{prelude::IdentityResolver, smol_str::ToSmolStr}; 12 12 #[allow(unused_imports)] 13 13 use jacquard::{ 14 14 smol_str::SmolStr, ··· 20 20 21 21 #[component] 22 22 pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) -> Element { 23 + let ident_clone = ident.clone(); 23 24 let entry = use_resource(use_reactive!(|(ident, book_title, title)| async move { 24 25 let fetcher = use_context::<fetch::CachedFetcher>(); 25 26 let entry = fetcher ··· 50 51 match &*entry.read_unchecked() { 51 52 Some(Some(entry_data)) => { 52 53 rsx! { EntryMarkdownDirect { 53 - content: entry_data.1.clone() 54 + content: entry_data.1.clone(), 55 + ident: ident_clone 54 56 } } 55 - }, 57 + } 56 58 Some(None) => { 57 59 rsx! { div { class: "error", "Entry not found" } } 58 60 } 59 - None => rsx! { p { "Loading..." } } 61 + None => rsx! { p { "Loading..." } }, 60 62 } 61 63 } 62 64 ··· 98 100 #[props(default)] id: String, 99 101 #[props(default = "entry".to_string())] class: String, 100 102 content: entry::Entry<'static>, 103 + ident: AtIdentifier<'static>, 101 104 ) -> Element { 102 - let parser = markdown_weaver::Parser::new(&content.content); 105 + use n0_future::stream::StreamExt; 106 + use weaver_renderer::{ 107 + atproto::{ClientContext, ClientWriter}, 108 + ContextIterator, NotebookProcessor, 109 + }; 103 110 104 - let mut html_buf = String::new(); 105 - markdown_weaver::html::push_html(&mut html_buf, parser); 111 + let processed = use_resource(use_reactive!(|(content, ident)| async move { 112 + // Create client context for link/image/embed handling 113 + let fetcher = use_context::<fetch::CachedFetcher>(); 114 + let did = match ident { 115 + AtIdentifier::Did(d) => d, 116 + AtIdentifier::Handle(h) => fetcher.client.resolve_handle(&h).await.ok()?, 117 + }; 118 + let ctx = ClientContext::<()>::new(content.clone(), did); 119 + let parser = markdown_weaver::Parser::new(&content.content); 120 + let iter = ContextIterator::default(parser); 121 + let processor = NotebookProcessor::new(ctx, iter); 106 122 107 - rsx! { 108 - div { 109 - id: "{id}", 110 - class: "{class}", 111 - dangerous_inner_html: "{html_buf}" 112 - } 123 + // Collect events from the processor stream 124 + let events: Vec<_> = StreamExt::collect(processor).await; 125 + 126 + // Render to HTML 127 + let mut html_buf = String::new(); 128 + let _ = ClientWriter::<_, ()>::new(&mut html_buf).run(events.into_iter()); 129 + Some(html_buf) 130 + })); 131 + 132 + match &*processed.read_unchecked() { 133 + Some(Some(html_buf)) => rsx! { 134 + div { 135 + id: "{id}", 136 + class: "{class}", 137 + dangerous_inner_html: "{html_buf}" 138 + } 139 + }, 140 + _ => rsx! { 141 + div { 142 + id: "{id}", 143 + class: "{class}", 144 + "Loading..." 145 + } 146 + }, 113 147 } 114 148 } 115 149