···45// Re-export jacquard for convenience
6pub use jacquard;
7-use jacquard::CowStr;
8-pub use jacquard_api;
0910pub use error::WeaverError;
01112-use jacquard::client::{Agent, AgentSession};
13-use jacquard::types::blob::BlobRef;
14-use jacquard::types::string::{AtUri, Cid, Did};
15-use jacquard_api::sh_weaver::notebook::{book, chapter, entry};
00016use std::path::Path;
00000000001718/// Extension trait providing weaver-specific multi-step operations on Agent
19///
···26/// - `agent.upload_blob()` - Upload a single blob
27///
28/// This trait is for multi-step workflows that coordinate between multiple operations.
29-#[trait_variant::make(Send)]
30-pub trait WeaverExt {
31 /// Publish a notebook directory to the user's PDS
32 ///
33 /// Multi-step workflow:
···38 /// 5. Create book record with entry refs
39 ///
40 /// Returns the AT-URI of the published book
41- async fn publish_notebook(&self, path: &Path) -> Result<PublishResult<'_>, WeaverError>;
0004243- /// Upload assets from markdown content
44 ///
45 /// Multi-step workflow:
46- /// 1. Parse markdown for image/asset refs
47- /// 2. Upload each asset → BlobRef
48- /// 3. Return mapping of original path → BlobRef
49 ///
50- /// Used by renderer to transform local refs to atproto refs
51- async fn upload_assets(
000000052 &self,
53- markdown: &str,
54- ) -> Result<Vec<(String, BlobRef<'_>)>, WeaverError>;
55}
5657-impl<A: AgentSession> WeaverExt for Agent<A> {
58 async fn publish_notebook(&self, _path: &Path) -> Result<PublishResult<'_>, WeaverError> {
59 // TODO: Implementation
60 todo!("publish_notebook not yet implemented")
61 }
6263- async fn upload_assets(
64 &self,
65- _markdown: &str,
66- ) -> Result<Vec<(String, BlobRef<'_>)>, WeaverError> {
67- // TODO: Implementation
68- todo!("upload_assets not yet implemented")
0000000000000000000000000000000000000000000000000000000000000000000000000069 }
70}
71···77 /// CID of the book record
78 pub cid: Cid<'a>,
79 /// URIs of published entries
80- pub entries: Vec<AtUri<'a>>,
81}
8283/// too many cows, so we have conversions
···173 Some(CowStr::Borrowed(aturi))
174 }
175}
00000000000000000000000000000000000000000000000000000000000000000000
···45// Re-export jacquard for convenience
6pub use jacquard;
7+use jacquard::error::ClientError;
8+use jacquard::types::ident::AtIdentifier;
9+use jacquard::{CowStr, IntoStatic, xrpc};
1011pub use error::WeaverError;
12+use jacquard::types::tid::{Ticker, Tid};
1314+use jacquard::bytes::Bytes;
15+use jacquard::client::{Agent, AgentError, AgentErrorKind, AgentSession, AgentSessionExt};
16+use jacquard::prelude::*;
17+use jacquard::smol_str::SmolStr;
18+use jacquard::types::blob::{BlobRef, MimeType};
19+use jacquard::types::string::{AtUri, Cid, Did, Handle, RecordKey};
20+use jacquard::xrpc::Response;
21use std::path::Path;
22+use std::sync::LazyLock;
23+use tokio::sync::Mutex;
24+use weaver_api::com_atproto::repo::get_record::GetRecordResponse;
25+use weaver_api::com_atproto::repo::strong_ref::StrongRef;
26+use weaver_api::sh_weaver::notebook::{book, chapter, entry};
27+use weaver_api::sh_weaver::publish::blob::Blob as PublishedBlob;
28+29+use crate::error::ParseError;
30+31+static W_TICKER: LazyLock<Mutex<Ticker>> = LazyLock::new(|| Mutex::new(Ticker::new()));
3233/// Extension trait providing weaver-specific multi-step operations on Agent
34///
···41/// - `agent.upload_blob()` - Upload a single blob
42///
43/// This trait is for multi-step workflows that coordinate between multiple operations.
44+//#[trait_variant::make(Send)]
45+pub trait WeaverExt: AgentSessionExt {
46 /// Publish a notebook directory to the user's PDS
47 ///
48 /// Multi-step workflow:
···53 /// 5. Create book record with entry refs
54 ///
55 /// Returns the AT-URI of the published book
56+ fn publish_notebook(
57+ &self,
58+ path: &Path,
59+ ) -> impl Future<Output = Result<PublishResult<'_>, WeaverError>>;
6061+ /// Publish a blob to the user's PDS
62 ///
63 /// Multi-step workflow:
64+ /// 1. Upload blob to PDS
65+ /// 2. Create blob record with CID
066 ///
67+ /// Returns the AT-URI of the published blob
68+ fn publish_blob<'a>(
69+ &self,
70+ blob: Bytes,
71+ url_path: &'a str,
72+ prev: Option<Tid>,
73+ ) -> impl Future<Output = Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError>>;
74+75+ fn confirm_record_ref(
76 &self,
77+ uri: &AtUri<'_>,
78+ ) -> impl Future<Output = Result<StrongRef<'_>, WeaverError>>;
79}
8081+impl<A: AgentSession + IdentityResolver> WeaverExt for Agent<A> {
82 async fn publish_notebook(&self, _path: &Path) -> Result<PublishResult<'_>, WeaverError> {
83 // TODO: Implementation
84 todo!("publish_notebook not yet implemented")
85 }
8687+ async fn publish_blob<'a>(
88 &self,
89+ blob: Bytes,
90+ url_path: &'a str,
91+ prev: Option<Tid>,
92+ ) -> Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError> {
93+ let mime_type = MimeType::new_owned(tree_magic::from_u8(blob.as_ref()));
94+95+ let blob = self.upload_blob(blob, mime_type).await?;
96+ let publish_record = PublishedBlob::new()
97+ .path(url_path)
98+ .upload(BlobRef::Blob(blob))
99+ .build();
100+ let tid = W_TICKER.lock().await.next(prev);
101+ let record = self
102+ .create_record(publish_record.clone(), Some(RecordKey::any(tid.as_str())?))
103+ .await?;
104+ let strong_ref = StrongRef::new().uri(record.uri).cid(record.cid).build();
105+106+ Ok((strong_ref, publish_record))
107+ }
108+109+ async fn confirm_record_ref(&self, uri: &AtUri<'_>) -> Result<StrongRef<'_>, WeaverError> {
110+ let rkey = uri.rkey().ok_or_else(|| {
111+ AgentError::from(
112+ ClientError::invalid_request("AtUri missing rkey")
113+ .with_help("ensure the URI includes a record key after the collection"),
114+ )
115+ })?;
116+117+ // Resolve authority (DID or handle) to get DID and PDS
118+ use jacquard::types::ident::AtIdentifier;
119+ let (repo_did, pds_url) = match uri.authority() {
120+ AtIdentifier::Did(did) => {
121+ let pds = self.pds_for_did(did).await.map_err(|e| {
122+ AgentError::from(
123+ ClientError::from(e)
124+ .with_context("DID document resolution failed during record retrieval"),
125+ )
126+ })?;
127+ (did.clone(), pds)
128+ }
129+ AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| {
130+ AgentError::from(
131+ ClientError::from(e)
132+ .with_context("handle resolution failed during record retrieval"),
133+ )
134+ })?,
135+ };
136+137+ // Make stateless XRPC call to that PDS (no auth required for public records)
138+ use weaver_api::com_atproto::repo::get_record::GetRecord;
139+ let request = GetRecord::new()
140+ .repo(AtIdentifier::Did(repo_did))
141+ .collection(
142+ uri.collection()
143+ .expect("collection should exist if rkey does")
144+ .clone(),
145+ )
146+ .rkey(rkey.clone())
147+ .build();
148+149+ let response: Response<GetRecordResponse> = {
150+ let http_request = xrpc::build_http_request(&pds_url, &request, &self.opts().await)
151+ .map_err(|e| AgentError::from(ClientError::transport(e)))?;
152+153+ let http_response = self
154+ .send_http(http_request)
155+ .await
156+ .map_err(|e| AgentError::from(ClientError::transport(e)))?;
157+158+ xrpc::process_response(http_response)
159+ }
160+ .map_err(|e| AgentError::new(AgentErrorKind::Client, Some(e.into())))?;
161+ let record = response.parse().map_err(|e| AgentError::xrpc(e))?;
162+ let strong_ref = StrongRef::new()
163+ .uri(record.uri)
164+ .cid(record.cid.expect("when does this NOT have a CID?"))
165+ .build();
166+ Ok(strong_ref.into_static())
167 }
168}
169···175 /// CID of the book record
176 pub cid: Cid<'a>,
177 /// URIs of published entries
178+ pub entries: Vec<StrongRef<'a>>,
179}
180181/// too many cows, so we have conversions
···271 Some(CowStr::Borrowed(aturi))
272 }
273}
274+275+pub enum LinkUri<'a> {
276+ AtRecord(AtUri<'a>),
277+ AtIdent(Did<'a>, Handle<'a>),
278+ Web(jacquard::url::Url),
279+ Path(markdown_weaver::CowStr<'a>),
280+ Heading(markdown_weaver::CowStr<'a>),
281+ Footnote(markdown_weaver::CowStr<'a>),
282+}
283+284+impl<'a> LinkUri<'a> {
285+ pub async fn resolve<A>(dest_url: &'a str, agent: &Agent<A>) -> LinkUri<'a>
286+ where
287+ A: AgentSession + IdentityResolver,
288+ {
289+ if dest_url.starts_with('@') {
290+ if let Ok(handle) = Handle::new(dest_url) {
291+ if let Ok(did) = agent.resolve_handle(&handle).await {
292+ return Self::AtIdent(did, handle);
293+ }
294+ }
295+ } else if dest_url.starts_with("did:") {
296+ if let Ok(did) = Did::new(dest_url) {
297+ if let Ok(doc) = agent.resolve_did_doc(&did).await {
298+ if let Ok(doc) = doc.parse_validated() {
299+ if let Some(handle) = doc.handles().first() {
300+ return Self::AtIdent(did, handle.clone());
301+ }
302+ }
303+ }
304+ }
305+ } else if dest_url.starts_with('#') {
306+ // local fragment
307+ return Self::Heading(markdown_weaver::CowStr::Borrowed(dest_url));
308+ } else if dest_url.starts_with('^') {
309+ // footnote
310+ return Self::Footnote(markdown_weaver::CowStr::Borrowed(dest_url));
311+ }
312+ if let Ok(url) = jacquard::url::Url::parse(dest_url) {
313+ if let Some(uri) = jacquard::richtext::extract_at_uri_from_url(
314+ url.as_str(),
315+ jacquard::richtext::DEFAULT_EMBED_DOMAINS,
316+ ) {
317+ if let AtIdentifier::Handle(handle) = uri.authority() {
318+ if let Ok(did) = agent.resolve_handle(handle).await {
319+ let mut aturi = format!("at://{did}");
320+ if let Some(collection) = uri.collection() {
321+ aturi.push_str(&format!("/{}", collection));
322+ if let Some(record) = uri.rkey() {
323+ aturi.push_str(&format!("/{}", record.0));
324+ }
325+ }
326+ if let Ok(aturi) = AtUri::new_owned(aturi) {
327+ return Self::AtRecord(aturi);
328+ }
329+ }
330+ return Self::AtRecord(uri);
331+ } else {
332+ return Self::AtRecord(uri);
333+ }
334+ } else if url.scheme() == "http" || url.scheme() == "https" {
335+ return Self::Web(url);
336+ }
337+ }
338+339+ LinkUri::Path(markdown_weaver::CowStr::Borrowed(dest_url))
340+ }
341+}
-3
crates/weaver-renderer/src/lib.rs
···3//! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS.
4//!
56-use async_trait::async_trait;
7use markdown_weaver::CowStr;
8use markdown_weaver::Event;
9-use markdown_weaver::LinkType;
10use markdown_weaver::Tag;
11use n0_future::Stream;
12-use n0_future::StreamExt;
13use n0_future::pin;
14use n0_future::stream::once_future;
15use yaml_rust2::Yaml;
···3//! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS.
4//!
506use markdown_weaver::CowStr;
7use markdown_weaver::Event;
08use markdown_weaver::Tag;
9use n0_future::Stream;
010use n0_future::pin;
11use n0_future::stream::once_future;
12use yaml_rust2::Yaml;