and now using the preliminary atproto-aware renderer.

Orual 974cc1e0 8e78c389

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