diff --git a/Cargo.lock b/Cargo.lock index 30caf12..e7aa48b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8624,6 +8624,7 @@ dependencies = [ "jacquard-axum", "markdown-weaver", "mini-moka", + "n0-future", "time", "weaver-api", "weaver-common", diff --git a/crates/weaver-renderer/src/atproto/client.rs b/crates/weaver-renderer/src/atproto/client.rs index 2a176fc..1d18b29 100644 --- a/crates/weaver-renderer/src/atproto/client.rs +++ b/crates/weaver-renderer/src/atproto/client.rs @@ -115,6 +115,20 @@ impl EmbedResolver for DefaultEmbedResolver< } } +impl EmbedResolver for () { + async fn resolve_profile(&self, uri: &AtUri<'_>) -> Result { + Ok("".to_string()) + } + + async fn resolve_post(&self, uri: &AtUri<'_>) -> Result { + Ok("".to_string()) + } + + async fn resolve_markdown(&self, url: &str, depth: usize) -> Result { + Ok("".to_string()) + } +} + const MAX_EMBED_DEPTH: usize = 3; pub struct ClientContext<'a, R = ()> { @@ -134,12 +148,12 @@ pub struct ClientContext<'a, R = ()> { title: MdCowStr<'a>, } -impl<'a> ClientContext<'a, ()> { - pub fn new(entry: Entry<'a>, creator_did: Did<'a>) -> Self { +impl<'a, R: EmbedResolver> ClientContext<'a, R> { + pub fn new(entry: Entry<'a>, creator_did: Did<'a>) -> ClientContext<'a, ()> { let blob_map = Self::build_blob_map(&entry); let title = MdCowStr::Boxed(entry.title.as_ref().into()); - Self { + ClientContext { entry, creator_did, blob_map, @@ -151,7 +165,7 @@ impl<'a> ClientContext<'a, ()> { } /// Add an embed resolver for fetching embed content - pub fn with_embed_resolver(self, resolver: Arc) -> ClientContext<'a, R> { + pub fn with_embed_resolver(self, resolver: Arc) -> ClientContext<'a, R> { ClientContext { entry: self.entry, creator_did: self.creator_did, @@ -164,7 +178,7 @@ impl<'a> ClientContext<'a, ()> { } } -impl<'a, R> ClientContext<'a, R> { +impl<'a, R: EmbedResolver> ClientContext<'a, R> { /// Create a child context with incremented embed depth (for recursive embeds) fn with_depth(&self, depth: usize) -> Self where @@ -432,7 +446,7 @@ mod tests { .created_at(Datetime::now()) .build(); - let ctx = ClientContext::new(entry, Did::new("did:plc:test").unwrap()); + let ctx = ClientContext::<()>::new(entry, Did::new("did:plc:test").unwrap()); assert_eq!(ctx.title.as_ref(), "Test"); } diff --git a/crates/weaver-renderer/src/atproto/writer.rs b/crates/weaver-renderer/src/atproto/writer.rs index f1659bf..1ec319f 100644 --- a/crates/weaver-renderer/src/atproto/writer.rs +++ b/crates/weaver-renderer/src/atproto/writer.rs @@ -6,7 +6,7 @@ use markdown_weaver::{ Alignment, BlockQuoteKind, CodeBlockKind, CowStr, EmbedType, Event, LinkType, Tag, }; -use markdown_weaver_escape::{escape_href, escape_html, escape_html_body_text, StrWrite}; +use markdown_weaver_escape::{StrWrite, escape_href, escape_html, escape_html_body_text}; use std::collections::HashMap; /// Synchronous callback for injecting embed content @@ -16,6 +16,12 @@ pub trait EmbedContentProvider { fn get_embed_content(&self, tag: &Tag<'_>) -> Option; } +impl EmbedContentProvider for () { + fn get_embed_content(&self, _tag: &Tag<'_>) -> Option { + None + } +} + /// Simple writer that outputs HTML from markdown events /// /// This writer is designed for client-side rendering where embeds may have @@ -40,7 +46,7 @@ enum TableState { Body, } -impl ClientWriter { +impl ClientWriter { pub fn new(writer: W) -> Self { Self { writer, @@ -55,7 +61,7 @@ impl ClientWriter { } /// Add an embed content provider - pub fn with_embed_provider(self, provider: E) -> ClientWriter { + pub fn with_embed_provider(self, provider: E) -> ClientWriter { ClientWriter { writer: self.writer, end_newline: self.end_newline, @@ -162,7 +168,12 @@ impl ClientWriter { self.write("\n

") } } - Tag::Heading { level, id, classes, attrs } => { + Tag::Heading { + level, + id, + classes, + attrs, + } => { if !self.end_newline { self.write("\n")?; } @@ -314,7 +325,12 @@ impl ClientWriter { Tag::Emphasis => self.write(""), Tag::Strong => self.write(""), Tag::Strikethrough => self.write(""), - Tag::Link { link_type: LinkType::Email, dest_url, title, .. } => { + Tag::Link { + link_type: LinkType::Email, + dest_url, + title, + .. + } => { self.write(" ClientWriter { } self.write("\">") } - Tag::Link { dest_url, title, .. } => { + Tag::Link { + dest_url, title, .. + } => { self.write(" ClientWriter { } self.write("\">") } - Tag::Image { dest_url, title, attrs, .. } => { + Tag::Image { + dest_url, + title, + attrs, + .. + } => { self.write("\"")?; ClientWriter { } self.write(" />") } - Tag::Embed { embed_type, dest_url, title, id, attrs } => { - self.write_embed(embed_type, dest_url, title, id, attrs) - } + Tag::Embed { + embed_type, + dest_url, + title, + id, + attrs, + } => self.write_embed(embed_type, dest_url, title, id, attrs), Tag::WeaverBlock(_, _) => { self.in_non_writing_block = true; Ok(()) @@ -441,7 +468,9 @@ impl ClientWriter { } } } +} +impl ClientWriter { fn write_embed( &mut self, embed_type: EmbedType, @@ -452,7 +481,9 @@ impl ClientWriter { ) -> Result<(), W::Error> { // Try to get content from attributes first let content_from_attrs = if let Some(ref attrs) = attrs { - attrs.attrs.iter() + attrs + .attrs + .iter() .find(|(k, _)| k.as_ref() == "content") .map(|(_, v)| v.as_ref().to_string()) } else { @@ -515,7 +546,6 @@ impl ClientWriter { } self.write(">")?; } - Ok(()) } } diff --git a/crates/weaver-server/Cargo.toml b/crates/weaver-server/Cargo.toml index 82314ca..d234d8f 100644 --- a/crates/weaver-server/Cargo.toml +++ b/crates/weaver-server/Cargo.toml @@ -25,6 +25,7 @@ weaver-api = { path = "../weaver-api", features = ["streaming"] } markdown-weaver = { workspace = true } weaver-renderer = { path = "../weaver-renderer" } mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } +n0-future = { workspace = true } #dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } axum = {version = "0.8.6", optional = true} diff --git a/crates/weaver-server/src/components/entry.rs b/crates/weaver-server/src/components/entry.rs index 6c4509e..4e046a0 100644 --- a/crates/weaver-server/src/components/entry.rs +++ b/crates/weaver-server/src/components/entry.rs @@ -8,7 +8,7 @@ use dioxus::{ fullstack::{get_server_url, reqwest}, prelude::*, }; -use jacquard::smol_str::ToSmolStr; +use jacquard::{prelude::IdentityResolver, smol_str::ToSmolStr}; #[allow(unused_imports)] use jacquard::{ smol_str::SmolStr, @@ -20,6 +20,7 @@ use weaver_api::sh_weaver::notebook::{entry, BookEntryView}; #[component] pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) -> Element { + let ident_clone = ident.clone(); let entry = use_resource(use_reactive!(|(ident, book_title, title)| async move { let fetcher = use_context::(); let entry = fetcher @@ -50,13 +51,14 @@ pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) match &*entry.read_unchecked() { Some(Some(entry_data)) => { rsx! { EntryMarkdownDirect { - content: entry_data.1.clone() + content: entry_data.1.clone(), + ident: ident_clone } } - }, + } Some(None) => { rsx! { div { class: "error", "Entry not found" } } } - None => rsx! { p { "Loading..." } } + None => rsx! { p { "Loading..." } }, } } @@ -98,18 +100,50 @@ fn EntryMarkdownDirect( #[props(default)] id: String, #[props(default = "entry".to_string())] class: String, content: entry::Entry<'static>, + ident: AtIdentifier<'static>, ) -> Element { - let parser = markdown_weaver::Parser::new(&content.content); + use n0_future::stream::StreamExt; + use weaver_renderer::{ + atproto::{ClientContext, ClientWriter}, + ContextIterator, NotebookProcessor, + }; - let mut html_buf = String::new(); - markdown_weaver::html::push_html(&mut html_buf, parser); + let processed = use_resource(use_reactive!(|(content, ident)| async move { + // Create client context for link/image/embed handling + let fetcher = use_context::(); + let did = match ident { + AtIdentifier::Did(d) => d, + AtIdentifier::Handle(h) => fetcher.client.resolve_handle(&h).await.ok()?, + }; + let ctx = ClientContext::<()>::new(content.clone(), did); + let parser = markdown_weaver::Parser::new(&content.content); + let iter = ContextIterator::default(parser); + let processor = NotebookProcessor::new(ctx, iter); - rsx! { - div { - id: "{id}", - class: "{class}", - dangerous_inner_html: "{html_buf}" - } + // Collect events from the processor stream + let events: Vec<_> = StreamExt::collect(processor).await; + + // Render to HTML + let mut html_buf = String::new(); + let _ = ClientWriter::<_, ()>::new(&mut html_buf).run(events.into_iter()); + Some(html_buf) + })); + + match &*processed.read_unchecked() { + Some(Some(html_buf)) => rsx! { + div { + id: "{id}", + class: "{class}", + dangerous_inner_html: "{html_buf}" + } + }, + _ => rsx! { + div { + id: "{id}", + class: "{class}", + "Loading..." + } + }, } }