···1+//! Feature-gated data fetching layer that abstracts over SSR and client-only modes.
2+//!
3+//! In fullstack-server mode, hooks use `use_server_future` with inline async closures.
4+//! In client-only mode, hooks use `use_resource` with context-provided fetchers.
5+6+#[cfg(feature = "server")]
7+use crate::blobcache::BlobCache;
8+use dioxus::prelude::*;
9+#[cfg(feature = "fullstack-server")]
10+#[allow(unused_imports)]
11+use dioxus::{fullstack::extract::Extension, CapturedError};
12+use jacquard::types::{did::Did, string::Handle};
13+#[allow(unused_imports)]
14+use jacquard::{
15+ prelude::IdentityResolver,
16+ smol_str::SmolStr,
17+ types::{cid::Cid, string::AtIdentifier},
18+};
19+#[allow(unused_imports)]
20+use std::sync::Arc;
21+use weaver_api::sh_weaver::notebook::{entry::Entry, BookEntryView};
22+// ============================================================================
23+// Wrapper Hooks (feature-gated)
24+// ============================================================================
25+26+/// Fetches entry data with SSR support in fullstack mode.
27+/// Returns a MappedSignal over the server future resource.
28+#[cfg(feature = "fullstack-server")]
29+pub fn use_entry_data(
30+ ident: AtIdentifier<'static>,
31+ book_title: SmolStr,
32+ title: SmolStr,
33+) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> {
34+ let fetcher = use_context::<crate::fetch::CachedFetcher>();
35+ let fetcher = fetcher.clone();
36+ let ident = use_signal(|| ident);
37+ let book_title = use_signal(|| book_title);
38+ let entry_title = use_signal(|| title);
39+ let res = use_server_future(move || {
40+ let fetcher = fetcher.clone();
41+ async move {
42+ if let Some(entry) = fetcher
43+ .get_entry(ident(), book_title(), entry_title())
44+ .await
45+ .ok()
46+ .flatten()
47+ {
48+ let (_book_entry_view, entry_record) = (&entry.0, &entry.1);
49+ if let Some(embeds) = &entry_record.embeds {
50+ if let Some(images) = &embeds.images {
51+ let ident = ident.clone();
52+ let images = images.clone();
53+ for image in &images.images {
54+ use jacquard::smol_str::ToSmolStr;
55+56+ let cid = image.image.blob().cid();
57+ cache_blob(
58+ ident.to_smolstr(),
59+ cid.to_smolstr(),
60+ image.name.as_ref().map(|n| n.to_smolstr()),
61+ )
62+ .await
63+ .ok();
64+ }
65+ }
66+ }
67+ Some((
68+ serde_json::to_value(entry.0.clone()).unwrap(),
69+ serde_json::to_value(entry.1.clone()).unwrap(),
70+ ))
71+ } else {
72+ None
73+ }
74+ }
75+ });
76+ res.map(|r| {
77+ use_memo(move || {
78+ if let Some(Some((ev, e))) = &*r.read_unchecked() {
79+ use jacquard::from_json_value;
80+81+ let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap();
82+ let entry = from_json_value::<Entry>(e.clone()).unwrap();
83+84+ Some((book_entry, entry))
85+ } else {
86+ None
87+ }
88+ })
89+ })
90+}
91+92+/// Fetches entry data client-side only (no SSR).
93+#[cfg(not(feature = "fullstack-server"))]
94+pub fn use_entry_data(
95+ ident: AtIdentifier<'static>,
96+ book_title: SmolStr,
97+ title: SmolStr,
98+) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> {
99+ let fetcher = use_context::<crate::fetch::CachedFetcher>();
100+ let fetcher = fetcher.clone();
101+ let ident = use_signal(|| ident);
102+ let book_title = use_signal(|| book_title);
103+ let entry_title = use_signal(|| title);
104+ let r = use_resource(move || {
105+ let fetcher = fetcher.clone();
106+ async move {
107+ fetcher
108+ .get_entry(ident(), book_title(), entry_title())
109+ .await
110+ .ok()
111+ .flatten()
112+ .map(|arc| (arc.0.clone(), arc.1.clone()))
113+ }
114+ });
115+ Ok(use_memo(move || {
116+ if let Some(Some((ev, e))) = &*r.read_unchecked() {
117+ Some((ev.clone(), e.clone()))
118+ } else {
119+ None
120+ }
121+ }))
122+}
123+124+pub fn use_handle(
125+ ident: AtIdentifier<'static>,
126+) -> Result<Memo<AtIdentifier<'static>>, RenderError> {
127+ let fetcher = use_context::<crate::fetch::CachedFetcher>();
128+ let fetcher = fetcher.clone();
129+ let ident = use_signal(|| ident);
130+ #[cfg(feature = "fullstack-server")]
131+ let h_str = {
132+ use_server_future(move || {
133+ let fetcher = fetcher.clone();
134+ async move {
135+ use jacquard::smol_str::ToSmolStr;
136+137+ fetcher
138+ .client
139+ .resolve_ident_owned(&ident())
140+ .await
141+ .map(|doc| doc.handles().first().map(|h| h.to_smolstr()))
142+ .ok()
143+ .flatten()
144+ }
145+ })
146+ };
147+ #[cfg(not(feature = "fullstack-server"))]
148+ let h_str = {
149+ use_resource(move || {
150+ let fetcher = fetcher.clone();
151+ async move {
152+ use jacquard::smol_str::ToSmolStr;
153+154+ fetcher
155+ .client
156+ .resolve_ident_owned(&ident())
157+ .await
158+ .map(|doc| doc.handles().first().map(|h| h.to_smolstr()))
159+ .ok()
160+ .flatten()
161+ }
162+ })
163+ };
164+ Ok(h_str.map(|h_str| {
165+ use_memo(move || {
166+ if let Some(Some(e)) = &*h_str.read_unchecked() {
167+ use jacquard::IntoStatic;
168+169+ AtIdentifier::Handle(Handle::raw(&e).into_static())
170+ } else {
171+ ident()
172+ }
173+ })
174+ })?)
175+}
176+177+/// Hook to render markdown client-side only (no SSR).
178+#[cfg(feature = "fullstack-server")]
179+pub fn use_rendered_markdown(
180+ content: Entry<'static>,
181+ ident: AtIdentifier<'static>,
182+) -> Result<Resource<Option<String>>, RenderError> {
183+ let ident = use_signal(|| ident);
184+ let content = use_signal(|| content);
185+ let fetcher = use_context::<crate::fetch::CachedFetcher>();
186+ Ok(use_server_future(move || {
187+ let fetcher = fetcher.clone();
188+ async move {
189+ let did = match ident() {
190+ AtIdentifier::Did(d) => d,
191+ AtIdentifier::Handle(h) => fetcher.client.resolve_handle(&h).await.ok()?,
192+ };
193+ Some(render_markdown_impl(content(), did).await)
194+ }
195+ })?)
196+}
197+198+/// Hook to render markdown client-side only (no SSR).
199+#[cfg(not(feature = "fullstack-server"))]
200+pub fn use_rendered_markdown(
201+ content: Entry<'static>,
202+ ident: AtIdentifier<'static>,
203+) -> Result<Resource<Option<String>>, RenderError> {
204+ let ident = use_signal(|| ident);
205+ let content = use_signal(|| content);
206+ let fetcher = use_context::<crate::fetch::CachedFetcher>();
207+ Ok(use_resource(move || {
208+ let fetcher = fetcher.clone();
209+ async move {
210+ let did = match ident() {
211+ AtIdentifier::Did(d) => d,
212+ AtIdentifier::Handle(h) => fetcher.client.resolve_handle(&h).await.ok()?,
213+ };
214+ Some(render_markdown_impl(content(), did).await)
215+ }
216+ }))
217+}
218+219+/// Internal implementation of markdown rendering.
220+async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> String {
221+ use n0_future::stream::StreamExt;
222+ use weaver_renderer::{
223+ atproto::{ClientContext, ClientWriter},
224+ ContextIterator, NotebookProcessor,
225+ };
226+227+ let ctx = ClientContext::<()>::new(content.clone(), did);
228+ let parser = markdown_weaver::Parser::new(&content.content);
229+ let iter = ContextIterator::default(parser);
230+ let processor = NotebookProcessor::new(ctx, iter);
231+232+ let events: Vec<_> = StreamExt::collect(processor).await;
233+234+ let mut html_buf = String::new();
235+ let _ = ClientWriter::<_, _, ()>::new(events.into_iter(), &mut html_buf).run();
236+ html_buf
237+}
238+239+#[cfg(feature = "fullstack-server")]
240+#[put("/cache/{ident}/{cid}?name", cache: Extension<Arc<BlobCache>>)]
241+pub async fn cache_blob(ident: SmolStr, cid: SmolStr, name: Option<SmolStr>) -> Result<()> {
242+ let ident = AtIdentifier::new_owned(ident)?;
243+ let cid = Cid::new_owned(cid.as_bytes())?;
244+ cache.cache(ident, cid, name).await
245+}
+3-1
crates/weaver-app/src/fetch.rs
···60 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> {
61 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
62 let (notebook, entries) = result.as_ref();
63- if let Some(entry) = cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) {
0064 Ok(Some(entry))
65 } else {
66 if let Some(entry) = entry_by_title(
···60 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> {
61 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
62 let (notebook, entries) = result.as_ref();
63+ if let Some(entry) =
64+ cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone()))
65+ {
66 Ok(Some(entry))
67 } else {
68 if let Some(entry) = entry_by_title(
+1
crates/weaver-app/src/main.rs
···22mod cache_impl;
23/// Define a components module that contains all shared components for our app.
24mod components;
025mod fetch;
26mod service_worker;
27/// Define a views module that contains the UI for all Layouts and Routes for our app.
···22mod cache_impl;
23/// Define a components module that contains all shared components for our app.
24mod components;
25+mod data;
26mod fetch;
27mod service_worker;
28/// Define a views module that contains the UI for all Layouts and Routes for our app.
+181-91
test.html
···1<!DOCTYPE html>
2-<html lang="en">
3 <head>
4- <meta charset="utf-8">
5- </head>
6- <script>
7- console.log( window.location.href ); // whatever your current location href is
8-window.history.replaceState( {} , 'foo', './LICENSE' );
9-console.log( window.location.href ); // oh, hey, it replaced the path with /foo
01000001112-</script>
13- <body>
14-<h1>Main Title & "Special Chars"</h1>
15-<p>This is a paragraph with <em>emphasis</em>, <strong>strong emphasis</strong>, <del>strikethrough</del>, and <code>inline code</code>.
16-Also, here's some escaped HTML: < & > " '</p>
17-<blockquote>
18-<p>A blockquote with
19-multiple lines.</p>
20-<blockquote>
21-<p>And a nested blockquote.</p>
22-</blockquote>
23-</blockquote>
24-<hr />
2526-<h2>Lists and Links</h2>
27-<ul>
28-<li><p>Unordered item 1</p>
29-</li>
30-<li><p>Unordered item 2</p>
31-<ul>
32-<li><p>Nested unordered item</p>
33-<ul>
34-<li><p>Deeply nested</p>
35-</li>
36-</ul>
37-</li>
38-</ul>
39-</li>
40-</ul>
41-<ol>
42-<li><p>Ordered item 1</p>
43-</li>
44-<li><p>Ordered item 2 (with a line break)
45-Still item 2.</p>
46-</li>
47-<li><p>Ordered item 3</p>
48-</li>
49-</ol>
50-<h3>GFM Task List</h3>
51-<ul>
52-<li><input type="checkbox" disabled /> <p>Unchecked task</p>
53-</li>
54-<li><input type="checkbox" disabled checked /> <p>Checked task</p>
55-</li>
56-</ul>
57-<p>Visit our site: <a href="http://example.com">http://example.com</a> or contact <a href="mailto:test@example.com">test@example.com</a>.
58-For more info, check <a href="http://www.example.org">www.example.org</a>.
59-A standard link: <a href="https://example.net" title="Link Title">Example Site</a>.
60-An angle link: <a href="https://angled.example.org">https://angled.example.org</a></p>
61-<h2>Media and Code</h2>
62-<p><img src="image.jpg" alt="Alt text for image" title="Image Title" /></p>
63-<pre><code class="language-rust">fn main() {
64- println!("Hello, Rust!");
65-}</code></pre>
66-<pre><code>Generic code block
67-with no language.</code></pre>
68-<h2>Table Time</h2>
69-<table>
70-<thead>
71-<tr>
72-<th style=\"text-align: left;\">Header 1</th>
73-<th style=\"text-align: center;\">Header 2 (Center)</th>
74-<th style=\"text-align: right;\">Header 3 (Right)</th>
75-</tr>
76-</thead>
77-<tbody>
78-<tr>
79-<td style=\"text-align: left;\">Cell 1-1</td>
80-<td style=\"text-align: center;\">Cell 1-2</td>
81-<td style=\"text-align: right;\">Cell 1-3</td>
82-</tr>
83-<tr>
84-<td style=\"text-align: left;\">Cell 2-1 with <code>inline</code></td>
85-<td style=\"text-align: center;\">Cell 2-2</td>
86-<td style=\"text-align: right;\"><em>Cell 2-3</em></td>
87-</tr>
88-</tbody>
89-</table>
90-<h2>Other Features</h2>
91-<p>This is a footnote reference.<sup class="footnote-reference"><a href="#fn:1">1</a></sup></p>
92-<p>Raw HTML:</p>
93-<div><p>Passthrough</p></div><p>Math: block</p>
94-<pre><code class="language-math math-display">\sum_{i=0}^n i = \frac{n(n+1)}{2}</code></pre>
95-<p>and inline <code class="language-math math-inline">E=mc^2</code>.</p>
96- </body>
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000097</html>