···1use jacquard::client::{Agent, FileAuthStore};
2-use jacquard::oauth::client::OAuthClient;
03use jacquard::oauth::loopback::LoopbackConfig;
4use jacquard::prelude::XrpcClient;
5use jacquard::types::ident::AtIdentifier;
···7use jacquard_api::app_bsky::actor::get_profile::GetProfile;
8use miette::{IntoDiagnostic, Result};
9use std::path::PathBuf;
01011use clap::{Parser, Subcommand};
1213#[derive(Parser)]
14-#[command(version, about, long_about = None)]
15#[command(propagate_version = true)]
16struct Cli {
000000000017 #[command(subcommand)]
18- command: Commands,
19}
2021#[derive(Subcommand)]
···29 #[arg(long)]
30 store: Option<PathBuf>,
31 },
32- /// Run a test command with stored auth
33- Run {
34- /// Path to auth store file
35- #[arg(long)]
36- store: Option<PathBuf>,
37- },
38}
3940#[tokio::main]
···44 let cli = Cli::parse();
4546 match cli.command {
47- Commands::Auth { handle, store } => {
48 let store_path = store.unwrap_or_else(default_auth_store_path);
49 authenticate(handle, store_path).await?;
50 }
51- Commands::Run { store } => {
52- let store_path = store.unwrap_or_else(default_auth_store_path);
53- run_test(store_path).await?;
0000000054 }
55 }
56···58}
5960async fn authenticate(handle: String, store_path: PathBuf) -> Result<()> {
61- println!("Authenticating with {}...", handle);
6263 let oauth = OAuthClient::with_default_config(FileAuthStore::new(&store_path));
64···70 let (did, session_id) = session.session_info().await;
7172 // Save DID and session_id for later use
73- let config_path = store_path.with_extension("toml");
74- let config_content = format!("did = \"{}\"\nsession_id = \"{}\"\n", did, session_id);
75 std::fs::write(&config_path, config_content).into_diagnostic()?;
7677 println!("Successfully authenticated!");
78 println!("Session saved to: {}", store_path.display());
79- println!("DID: {}", did);
8081 Ok(())
82}
8384-async fn run_test(store_path: PathBuf) -> Result<()> {
85- println!("Loading session from {}...", store_path.display());
008687- // Read DID and session_id from config
88- let config_path = store_path.with_extension("toml");
89- let config_content = std::fs::read_to_string(&config_path)
90- .into_diagnostic()
91- .map_err(|_| miette::miette!("No auth config found. Run 'weaver auth' first."))?;
9293- let did_line = config_content
94- .lines()
95- .find(|l| l.starts_with("did = "))
96- .ok_or_else(|| miette::miette!("Invalid config file"))?;
97- let session_id_line = config_content
98- .lines()
99- .find(|l| l.starts_with("session_id = "))
100- .ok_or_else(|| miette::miette!("Invalid config file"))?;
101102- let did_str = did_line
103- .trim_start_matches("did = \"")
104- .trim_end_matches('"');
105- let session_id = session_id_line
106- .trim_start_matches("session_id = \"")
107- .trim_end_matches('"');
108109- let did = jacquard::types::string::Did::new(did_str)
110- .map_err(|_| miette::miette!("Invalid DID in config"))?;
111112- let oauth = OAuthClient::with_default_config(FileAuthStore::new(&store_path));
113- let session = oauth.restore(&did, session_id).await.into_diagnostic()?;
114115- let agent = Agent::from(session);
000116117- println!("Fetching profile for {}...", did);
0000000118119- let profile = agent
120- .send(GetProfile::new().actor(AtIdentifier::Did(did)).build())
121- .await
122- .into_diagnostic()?
123- .into_output()
124- .into_diagnostic()?;
125126- println!("\nProfile:");
127- println!(" Handle: {}", profile.value.handle);
128- if let Some(display_name) = &profile.value.display_name {
129- println!(" Display Name: {}", display_name);
00130 }
131- if let Some(description) = &profile.value.description {
132- println!(" Description: {}", description);
0000133 }
0000000000000134135 Ok(())
136}
···1use jacquard::client::{Agent, FileAuthStore};
2+use jacquard::identity::JacquardResolver;
3+use jacquard::oauth::client::{OAuthClient, OAuthSession};
4use jacquard::oauth::loopback::LoopbackConfig;
5use jacquard::prelude::XrpcClient;
6use jacquard::types::ident::AtIdentifier;
···8use jacquard_api::app_bsky::actor::get_profile::GetProfile;
9use miette::{IntoDiagnostic, Result};
10use std::path::PathBuf;
11+use weaver_renderer::static_site::StaticSiteWriter;
1213use clap::{Parser, Subcommand};
1415#[derive(Parser)]
16+#[command(version, about = "Weaver - Static site generator for AT Protocol notebooks", long_about = None)]
17#[command(propagate_version = true)]
18struct Cli {
19+ /// Path to notebook directory
20+ source: Option<PathBuf>,
21+22+ /// Output directory for static site
23+ dest: Option<PathBuf>,
24+25+ /// Path to auth store file
26+ #[arg(long)]
27+ store: Option<PathBuf>,
28+29 #[command(subcommand)]
30+ command: Option<Commands>,
31}
3233#[derive(Subcommand)]
···41 #[arg(long)]
42 store: Option<PathBuf>,
43 },
00000044}
4546#[tokio::main]
···50 let cli = Cli::parse();
5152 match cli.command {
53+ Some(Commands::Auth { handle, store }) => {
54 let store_path = store.unwrap_or_else(default_auth_store_path);
55 authenticate(handle, store_path).await?;
56 }
57+ None => {
58+ // Render command (default)
59+ let source = cli.source.ok_or_else(|| {
60+ miette::miette!("Source directory required. Usage: weaver <source> <dest>")
61+ })?;
62+ let dest = cli.dest.ok_or_else(|| {
63+ miette::miette!("Destination directory required. Usage: weaver <source> <dest>")
64+ })?;
65+ let store_path = cli.store.unwrap_or_else(default_auth_store_path);
66+67+ render_notebook(source, dest, store_path).await?;
68 }
69 }
70···72}
7374async fn authenticate(handle: String, store_path: PathBuf) -> Result<()> {
75+ println!("Authenticating as @{handle} ...");
7677 let oauth = OAuthClient::with_default_config(FileAuthStore::new(&store_path));
78···84 let (did, session_id) = session.session_info().await;
8586 // Save DID and session_id for later use
87+ let config_path = store_path.with_extension("kdl");
88+ let config_content = format!("did \"{}\"\nsession-id \"{}\"\n", did, session_id);
89 std::fs::write(&config_path, config_content).into_diagnostic()?;
9091 println!("Successfully authenticated!");
92 println!("Session saved to: {}", store_path.display());
09394 Ok(())
95}
9697+async fn try_load_session(
98+ store_path: &PathBuf,
99+) -> Option<OAuthSession<JacquardResolver, FileAuthStore>> {
100+ use kdl::KdlDocument;
101102+ // Check if auth store exists
103+ if !store_path.exists() {
104+ return None;
105+ }
0106107+ // Read KDL config
108+ let config_path = store_path.with_extension("kdl");
109+ let config_content = std::fs::read_to_string(&config_path).ok()?;
00000110111+ // Parse KDL
112+ let doc: KdlDocument = config_content.parse().ok()?;
113+114+ // Extract did and session-id
115+ let did_node = doc.get("did")?;
116+ let session_id_node = doc.get("session-id")?;
117118+ let did_str = did_node.entries().first()?.value().as_string()?;
119+ let session_id = session_id_node.entries().first()?.value().as_string()?;
120121+ // Parse DID
122+ let did = jacquard::types::string::Did::new(did_str).ok()?;
123124+ // Restore OAuth session
125+ let oauth = OAuthClient::with_default_config(FileAuthStore::new(store_path));
126+ oauth.restore(&did, session_id).await.ok()
127+}
128129+async fn render_notebook(source: PathBuf, dest: PathBuf, store_path: PathBuf) -> Result<()> {
130+ // Validate source exists
131+ if !source.exists() {
132+ return Err(miette::miette!(
133+ "Source directory not found: {}",
134+ source.display()
135+ ));
136+ }
137138+ // Try to load session
139+ let session = try_load_session(&store_path).await;
0000140141+ // Log auth status
142+ if session.is_some() {
143+ println!("✓ Found authentication");
144+ } else {
145+ println!("⚠ No authentication found");
146+ println!(" Run 'weaver auth <handle>' to enable network features");
147 }
148+149+ // Create dest parent directories if needed
150+ if let Some(parent) = dest.parent() {
151+ if !parent.exists() {
152+ std::fs::create_dir_all(parent).into_diagnostic()?;
153+ }
154 }
155+156+ // Create renderer
157+ let writer = StaticSiteWriter::new(source, dest.clone(), session);
158+159+ // Render
160+ println!("→ Rendering notebook...");
161+ let start = std::time::Instant::now();
162+ writer.run().await?;
163+ let elapsed = start.elapsed();
164+165+ // Report success
166+ println!("✓ Rendered in {:.2}s", elapsed.as_secs_f64());
167+ println!("✓ Output: {}", dest.display());
168169 Ok(())
170}
+2-76
crates/weaver-renderer/src/base_html.rs
···583584/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
585/// push it to a `String`.
586-///
587-/// # Examples
588-///
589-/// ```
590-/// use pulldown_cmark::{html, Parser};
591-///
592-/// let markdown_str = r#"
593-/// hello
594-/// =====
595-///
596-/// * alpha
597-/// * beta
598-/// "#;
599-/// let parser = Parser::new(markdown_str);
600-///
601-/// let mut html_buf = String::new();
602-/// html::push_html(&mut html_buf, parser);
603-///
604-/// assert_eq!(html_buf, r#"<h1>hello</h1>
605-/// <ul>
606-/// <li>alpha</li>
607-/// <li>beta</li>
608-/// </ul>
609-/// "#);
610-/// ```
611pub fn push_html<'a, I>(s: &mut String, iter: I)
612where
613 I: Iterator<Item = Event<'a>>,
···622/// will result in poor performance. Wrap these in a
623/// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to
624/// prevent unnecessary slowdowns.
625-///
626-/// # Examples
627-///
628-/// ```
629-/// use pulldown_cmark::{html, Parser};
630-/// use std::io::Cursor;
631-///
632-/// let markdown_str = r#"
633-/// hello
634-/// =====
635-///
636-/// * alpha
637-/// * beta
638-/// "#;
639-/// let mut bytes = Vec::new();
640-/// let parser = Parser::new(markdown_str);
641-///
642-/// html::write_html_io(Cursor::new(&mut bytes), parser);
643-///
644-/// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1>
645-/// <ul>
646-/// <li>alpha</li>
647-/// <li>beta</li>
648-/// </ul>
649-/// "#);
650-/// ```
651//#[cfg(feature = "std")]
652pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
653where
···659660/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
661/// write it into Unicode-accepting buffer or stream.
662-///
663-/// # Examples
664-///
665-/// ```
666-/// use pulldown_cmark::{html, Parser};
667-///
668-/// let markdown_str = r#"
669-/// hello
670-/// =====
671-///
672-/// * alpha
673-/// * beta
674-/// "#;
675-/// let mut buf = String::new();
676-/// let parser = Parser::new(markdown_str);
677-///
678-/// html::write_html_fmt(&mut buf, parser);
679-///
680-/// assert_eq!(buf, r#"<h1>hello</h1>
681-/// <ul>
682-/// <li>alpha</li>
683-/// <li>beta</li>
684-/// </ul>
685-/// "#);
686-/// ```
687pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> core::fmt::Result
688where
689 I: Iterator<Item = Event<'a>>,
···583584/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
585/// push it to a `String`.
0000000000000000000000000586pub fn push_html<'a, I>(s: &mut String, iter: I)
587where
588 I: Iterator<Item = Event<'a>>,
···597/// will result in poor performance. Wrap these in a
598/// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to
599/// prevent unnecessary slowdowns.
600+0000000000000000000000000601//#[cfg(feature = "std")]
602pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
603where
···609610/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
611/// write it into Unicode-accepting buffer or stream.
612+000000000000000000000000613pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> core::fmt::Result
614where
615 I: Iterator<Item = Event<'a>>,
···1+---
2+source: crates/weaver-renderer/src/static_site.rs
3+expression: output
4+---
5+<p>This is a paragraph.</p>
6+<p>This is another paragraph.</p>