tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
and now using the preliminary atproto-aware renderer.
Orual
3 months ago
974cc1e0
8e78c389
+111
-31
5 changed files
expand all
collapse all
unified
split
Cargo.lock
crates
weaver-renderer
src
atproto
client.rs
writer.rs
weaver-server
Cargo.toml
src
components
entry.rs
+1
Cargo.lock
···
8624
"jacquard-axum",
8625
"markdown-weaver",
8626
"mini-moka",
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
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 } => {
0
0
0
0
0
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, .. } => {
0
0
0
0
0
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, .. } => {
0
0
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, .. } => {
0
0
0
0
0
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
-
}
0
0
0
0
368
Tag::WeaverBlock(_, _) => {
369
self.in_non_writing_block = true;
370
Ok(())
···
441
}
442
}
443
}
0
444
0
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()
0
0
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
}
0
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" }
0
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 {
0
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()
0
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>,
0
101
) -> Element {
102
-
let parser = markdown_weaver::Parser::new(&content.content);
0
0
0
0
103
104
-
let mut html_buf = String::new();
105
-
markdown_weaver::html::push_html(&mut html_buf, parser);
0
0
0
0
0
0
0
0
0
106
107
-
rsx! {
108
-
div {
109
-
id: "{id}",
110
-
class: "{class}",
111
-
dangerous_inner_html: "{html_buf}"
112
-
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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