some font stuff, bug fixes in the notebook iterator

Orual 5b07a7ff d280d866

+162 -34
+3
Cargo.lock
··· 8576 "miette 7.6.0", 8577 "n0-future", 8578 "tokio", 8579 "weaver-api", 8580 "weaver-common", 8581 "weaver-renderer", ··· 8634 "thiserror 2.0.17", 8635 "tokio", 8636 "tokio-util", 8637 "unicode-normalization", 8638 "url", 8639 "weaver-api",
··· 8576 "miette 7.6.0", 8577 "n0-future", 8578 "tokio", 8579 + "tracing", 8580 + "tracing-subscriber", 8581 "weaver-api", 8582 "weaver-common", 8583 "weaver-renderer", ··· 8636 "thiserror 2.0.17", 8637 "tokio", 8638 "tokio-util", 8639 + "tracing", 8640 "unicode-normalization", 8641 "url", 8642 "weaver-api",
+2
crates/weaver-cli/Cargo.toml
··· 26 tokio = { version = "1.45.0", features = ["full"] } 27 dirs = "6.0.0" 28 kdl = "4.6"
··· 26 tokio = { version = "1.45.0", features = ["full"] } 27 dirs = "6.0.0" 28 kdl = "4.6" 29 + tracing = "0.1" 30 + tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
+14
crates/weaver-cli/src/main.rs
··· 202 } 203 204 async fn publish_notebook(source: PathBuf, title: String, store_path: PathBuf) -> Result<()> { 205 println!("Publishing notebook from: {}", source.display()); 206 println!("Title: {}", title); 207 ··· 262 263 // Walk vault directory 264 println!("→ Scanning vault..."); 265 let contents = vault_contents(&source, WalkOptions::new())?; 266 267 // Convert to Arc first ··· 288 289 // Process each file 290 for file_path in &md_files { 291 println!("Processing: {}", file_path.display()); 292 293 // Read file content ··· 330 // Extract blobs and entry metadata 331 let blobs = file_context.blobs(); 332 let entry_title = file_context.entry_title(); 333 334 // Build Entry record with blobs 335 use jacquard::types::blob::BlobRef;
··· 202 } 203 204 async fn publish_notebook(source: PathBuf, title: String, store_path: PathBuf) -> Result<()> { 205 + // Initialize tracing for debugging 206 + tracing_subscriber::fmt() 207 + .with_env_filter( 208 + tracing_subscriber::EnvFilter::try_from_default_env() 209 + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug")) 210 + ) 211 + .init(); 212 + 213 println!("Publishing notebook from: {}", source.display()); 214 println!("Title: {}", title); 215 ··· 270 271 // Walk vault directory 272 println!("→ Scanning vault..."); 273 + tracing::debug!("Scanning directory: {}", source.display()); 274 let contents = vault_contents(&source, WalkOptions::new())?; 275 276 // Convert to Arc first ··· 297 298 // Process each file 299 for file_path in &md_files { 300 + let _span = tracing::info_span!("process_file", path = %file_path.display()).entered(); 301 println!("Processing: {}", file_path.display()); 302 303 // Read file content ··· 340 // Extract blobs and entry metadata 341 let blobs = file_context.blobs(); 342 let entry_title = file_context.entry_title(); 343 + 344 + if !blobs.is_empty() { 345 + tracing::debug!("Uploaded {} image(s)", blobs.len()); 346 + } 347 348 // Build Entry record with blobs 349 use jacquard::types::blob::BlobRef;
+1
crates/weaver-renderer/Cargo.toml
··· 21 unicode-normalization = "0.1.24" 22 yaml-rust2 = { version = "0.10.2" } 23 bitflags = "2.9.1" 24 25 dashmap = "6.1.0" 26 regex = "1.11.1"
··· 21 unicode-normalization = "0.1.24" 22 yaml-rust2 = { version = "0.10.2" } 23 bitflags = "2.9.1" 24 + tracing = "0.1" 25 26 dashmap = "6.1.0" 27 regex = "1.11.1"
+1
crates/weaver-renderer/src/atproto/client.rs
··· 131 132 const MAX_EMBED_DEPTH: usize = 3; 133 134 pub struct ClientContext<'a, R = ()> { 135 // Entry being rendered 136 entry: Entry<'a>,
··· 131 132 const MAX_EMBED_DEPTH: usize = 3; 133 134 + #[derive(Clone)] 135 pub struct ClientContext<'a, R = ()> { 136 // Entry being rendered 137 entry: Entry<'a>,
+8
crates/weaver-renderer/src/atproto/preprocess.rs
··· 159 .insert(self.current_path.clone(), frontmatter); 160 } 161 162 async fn handle_link<'s>(&self, link: Tag<'s>) -> Tag<'s> { 163 use crate::utils::lookup_filename_in_vault; 164 use weaver_common::LinkUri; ··· 234 } 235 } 236 237 async fn handle_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { 238 use crate::utils::is_local_path; 239 use jacquard::bytes::Bytes; ··· 260 .join(dest_url.as_ref()) 261 }; 262 263 if let Ok(image_data) = fs::read(&file_path).await { 264 // Derive blob name from filename 265 let filename = file_path 266 .file_stem() ··· 277 ); 278 279 // Upload blob (dereference Arc) 280 if let Ok(blob) = (*self.agent).upload_blob(bytes, mime.clone()).await { 281 use jacquard::IntoStatic; 282 ··· 316 } 317 } 318 319 async fn handle_embed<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 320 use crate::utils::lookup_filename_in_vault; 321 use weaver_common::LinkUri; ··· 460 461 let at_uri = format!("at://{}", did.as_ref()); 462 463 // Fetch and render the profile 464 let content = match fetch_and_render_profile(&did, &*self.agent).await { 465 Ok(html) => Some(html), ··· 494 use crate::atproto::{fetch_and_render_generic, fetch_and_render_post}; 495 use markdown_weaver::WeaverAttributes; 496 497 // Determine if this is a known type 498 let content = if let Some(collection) = uri.collection() { 499 match collection.as_ref() {
··· 159 .insert(self.current_path.clone(), frontmatter); 160 } 161 162 + #[tracing::instrument(skip(self, link), fields(dest = ?link))] 163 async fn handle_link<'s>(&self, link: Tag<'s>) -> Tag<'s> { 164 use crate::utils::lookup_filename_in_vault; 165 use weaver_common::LinkUri; ··· 235 } 236 } 237 238 + #[tracing::instrument(skip(self, image), fields(dest = ?image))] 239 async fn handle_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { 240 use crate::utils::is_local_path; 241 use jacquard::bytes::Bytes; ··· 262 .join(dest_url.as_ref()) 263 }; 264 265 + tracing::debug!("Reading image file: {}", file_path.display()); 266 if let Ok(image_data) = fs::read(&file_path).await { 267 + tracing::debug!("Read {} bytes from {}", image_data.len(), file_path.display()); 268 // Derive blob name from filename 269 let filename = file_path 270 .file_stem() ··· 281 ); 282 283 // Upload blob (dereference Arc) 284 + tracing::debug!("Uploading image blob: {} ({} bytes)", file_path.display(), bytes.len()); 285 if let Ok(blob) = (*self.agent).upload_blob(bytes, mime.clone()).await { 286 use jacquard::IntoStatic; 287 ··· 321 } 322 } 323 324 + #[tracing::instrument(skip(self, embed), fields(dest = ?embed))] 325 async fn handle_embed<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 326 use crate::utils::lookup_filename_in_vault; 327 use weaver_common::LinkUri; ··· 466 467 let at_uri = format!("at://{}", did.as_ref()); 468 469 + tracing::debug!("Fetching profile embed: {}", did.as_ref()); 470 // Fetch and render the profile 471 let content = match fetch_and_render_profile(&did, &*self.agent).await { 472 Ok(html) => Some(html), ··· 501 use crate::atproto::{fetch_and_render_generic, fetch_and_render_post}; 502 use markdown_weaver::WeaverAttributes; 503 504 + tracing::debug!("Fetching record embed: {}", uri.as_ref()); 505 // Determine if this is a known type 506 let content = if let Some(collection) = uri.collection() { 507 match collection.as_ref() {
+5 -5
crates/weaver-renderer/src/css.rs
··· 101 102 h1 {{ 103 font-size: 2rem; 104 - color: var(--color-primary); 105 }} 106 h2 {{ 107 font-size: 1.5rem; 108 - color: var(--color-secondary); 109 }} 110 h3 {{ 111 font-size: 1.25rem; 112 - color: var(--color-primary); 113 }} 114 h4 {{ 115 font-size: 1.2rem; 116 - color: var(--color-secondary); 117 }} 118 h5 {{ 119 font-size: 1.125rem; 120 - color: var(--color-primary); 121 }} 122 h6 {{ font-size: 1rem; }} 123
··· 101 102 h1 {{ 103 font-size: 2rem; 104 + color: var(--color-secondary); 105 }} 106 h2 {{ 107 font-size: 1.5rem; 108 + color: var(--color-primary); 109 }} 110 h3 {{ 111 font-size: 1.25rem; 112 + color: var(--color-secondary); 113 }} 114 h4 {{ 115 font-size: 1.2rem; 116 + color: var(--color-tertiary); 117 }} 118 h5 {{ 119 font-size: 1.125rem; 120 + color: var(--color-secondary); 121 }} 122 h6 {{ font-size: 1rem; }} 123
+108 -20
crates/weaver-renderer/src/lib.rs
··· 6 use markdown_weaver::CowStr; 7 use markdown_weaver::Event; 8 use markdown_weaver::Tag; 9 - use n0_future::pin; 10 - use n0_future::stream::once_future; 11 use n0_future::Stream; 12 use yaml_rust2::Yaml; 13 use yaml_rust2::YamlLoader; ··· 76 } 77 } 78 79 - #[derive(Debug, Default)] 80 #[pin_project::pin_project] 81 pub struct NotebookProcessor<'a, I: Iterator<Item = Event<'a>>, CTX> { 82 context: CTX, 83 iter: ContextIterator<'a, I>, 84 } 85 86 impl<'a, I: Iterator<Item = Event<'a>>, CTX> NotebookProcessor<'a, I, CTX> { 87 pub fn new(ctx: CTX, iter: ContextIterator<'a, I>) -> Self { 88 - Self { context: ctx, iter } 89 } 90 } 91 92 - impl<'a, I: Iterator<Item = Event<'a>>, CTX: NotebookContext> Stream 93 for NotebookProcessor<'a, I, CTX> 94 { 95 type Item = Event<'a>; ··· 98 self.iter.size_hint() 99 } 100 fn poll_next( 101 - self: Pin<&mut Self>, 102 cx: &mut std::task::Context<'_>, 103 ) -> Poll<Option<Self::Item>> { 104 - let this = self.project(); 105 let iter: &mut ContextIterator<'a, I> = this.iter; 106 if let Some((event, ctxt)) = iter.next() { 107 match ctxt { 108 EventContext::EmbedLink => match event { 109 Event::Start(ref tag) => match tag { 110 Tag::Embed { .. } => { 111 - let fut = once_future(this.context.handle_embed(tag.clone())); 112 - pin!(fut); 113 - fut.poll_next(cx) 114 - .map(|tag| tag.map(|t| Event::Start(t.into_static()))) 115 } 116 _ => Poll::Ready(Some(event)), 117 }, ··· 124 EventContext::Reference => match event { 125 Event::Start(ref tag) => match tag { 126 Tag::Link { .. } => { 127 - let fut = once_future(this.context.handle_link(tag.clone())); 128 - pin!(fut); 129 - fut.poll_next(cx).map(|tag| tag.map(|t| Event::Start(t))) 130 } 131 _ => Poll::Ready(Some(event)), 132 }, ··· 149 EventContext::Link => match event { 150 Event::Start(ref tag) => match tag { 151 Tag::Link { .. } => { 152 - let fut = once_future(this.context.handle_link(tag.clone())); 153 - pin!(fut); 154 - fut.poll_next(cx).map(|tag| tag.map(|t| Event::Start(t))) 155 } 156 _ => Poll::Ready(Some(event)), 157 }, ··· 160 EventContext::Image => match event { 161 Event::Start(ref tag) => match tag { 162 Tag::Image { .. } => { 163 - let fut = once_future(this.context.handle_image(tag.clone())); 164 - pin!(fut); 165 - fut.poll_next(cx).map(|tag| tag.map(|t| Event::Start(t))) 166 } 167 _ => Poll::Ready(Some(event)), 168 },
··· 6 use markdown_weaver::CowStr; 7 use markdown_weaver::Event; 8 use markdown_weaver::Tag; 9 use n0_future::Stream; 10 use yaml_rust2::Yaml; 11 use yaml_rust2::YamlLoader; ··· 74 } 75 } 76 77 #[pin_project::pin_project] 78 pub struct NotebookProcessor<'a, I: Iterator<Item = Event<'a>>, CTX> { 79 context: CTX, 80 iter: ContextIterator<'a, I>, 81 + #[pin] 82 + pending_future: Option<Pin<Box<dyn std::future::Future<Output = Event<'a>> + 'a>>>, 83 } 84 85 impl<'a, I: Iterator<Item = Event<'a>>, CTX> NotebookProcessor<'a, I, CTX> { 86 pub fn new(ctx: CTX, iter: ContextIterator<'a, I>) -> Self { 87 + Self { 88 + context: ctx, 89 + iter, 90 + pending_future: None, 91 + } 92 } 93 } 94 95 + impl<'a, I: Iterator<Item = Event<'a>>, CTX: NotebookContext + Clone + 'a> Stream 96 for NotebookProcessor<'a, I, CTX> 97 { 98 type Item = Event<'a>; ··· 101 self.iter.size_hint() 102 } 103 fn poll_next( 104 + mut self: Pin<&mut Self>, 105 cx: &mut std::task::Context<'_>, 106 ) -> Poll<Option<Self::Item>> { 107 + // First, poll any pending future to completion 108 + if let Some(fut) = self.as_mut().project().pending_future.as_mut().as_pin_mut() { 109 + match fut.poll(cx) { 110 + Poll::Ready(event) => { 111 + // Clear the future and return the result 112 + self.as_mut().project().pending_future.set(None); 113 + return Poll::Ready(Some(event)); 114 + } 115 + Poll::Pending => { 116 + // Keep the future for next poll 117 + return Poll::Pending; 118 + } 119 + } 120 + } 121 + 122 + let mut this = self.project(); 123 let iter: &mut ContextIterator<'a, I> = this.iter; 124 if let Some((event, ctxt)) = iter.next() { 125 match ctxt { 126 EventContext::EmbedLink => match event { 127 Event::Start(ref tag) => match tag { 128 Tag::Embed { .. } => { 129 + let tag_clone = tag.clone(); 130 + let ctx_clone = this.context.clone(); 131 + let fut = async move { 132 + let processed_tag = ctx_clone.handle_embed(tag_clone).await; 133 + Event::Start(processed_tag.into_static()) 134 + }; 135 + 136 + *this.pending_future = Some(Box::pin(fut)); 137 + 138 + if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 139 + match fut.poll(cx) { 140 + Poll::Ready(event) => { 141 + *this.pending_future = None; 142 + Poll::Ready(Some(event)) 143 + } 144 + Poll::Pending => Poll::Pending, 145 + } 146 + } else { 147 + unreachable!() 148 + } 149 } 150 _ => Poll::Ready(Some(event)), 151 }, ··· 158 EventContext::Reference => match event { 159 Event::Start(ref tag) => match tag { 160 Tag::Link { .. } => { 161 + let tag_clone = tag.clone(); 162 + let ctx_clone = this.context.clone(); 163 + let fut = async move { 164 + let processed_tag = ctx_clone.handle_link(tag_clone).await; 165 + Event::Start(processed_tag) 166 + }; 167 + 168 + *this.pending_future = Some(Box::pin(fut)); 169 + 170 + if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 171 + match fut.poll(cx) { 172 + Poll::Ready(event) => { 173 + *this.pending_future = None; 174 + Poll::Ready(Some(event)) 175 + } 176 + Poll::Pending => Poll::Pending, 177 + } 178 + } else { 179 + unreachable!() 180 + } 181 } 182 _ => Poll::Ready(Some(event)), 183 }, ··· 200 EventContext::Link => match event { 201 Event::Start(ref tag) => match tag { 202 Tag::Link { .. } => { 203 + let tag_clone = tag.clone(); 204 + let ctx_clone = this.context.clone(); 205 + let fut = async move { 206 + let processed_tag = ctx_clone.handle_link(tag_clone).await; 207 + Event::Start(processed_tag) 208 + }; 209 + 210 + *this.pending_future = Some(Box::pin(fut)); 211 + 212 + if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 213 + match fut.poll(cx) { 214 + Poll::Ready(event) => { 215 + *this.pending_future = None; 216 + Poll::Ready(Some(event)) 217 + } 218 + Poll::Pending => Poll::Pending, 219 + } 220 + } else { 221 + unreachable!() 222 + } 223 } 224 _ => Poll::Ready(Some(event)), 225 }, ··· 228 EventContext::Image => match event { 229 Event::Start(ref tag) => match tag { 230 Tag::Image { .. } => { 231 + // Create future that handles the image and wraps result in Event::Start 232 + let tag_clone = tag.clone(); 233 + let ctx_clone = this.context.clone(); 234 + let fut = async move { 235 + let processed_tag = ctx_clone.handle_image(tag_clone).await; 236 + Event::Start(processed_tag) 237 + }; 238 + 239 + // Store the future and poll it 240 + *this.pending_future = Some(Box::pin(fut)); 241 + 242 + // Immediately poll the stored future 243 + if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 244 + match fut.poll(cx) { 245 + Poll::Ready(event) => { 246 + *this.pending_future = None; 247 + Poll::Ready(Some(event)) 248 + } 249 + Poll::Pending => Poll::Pending, 250 + } 251 + } else { 252 + unreachable!() 253 + } 254 } 255 _ => Poll::Ready(Some(event)), 256 },
+6 -2
crates/weaver-renderer/src/static_site/writer.rs
··· 180 } 181 } 182 183 - impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession + IdentityResolver, W: StrWrite> 184 - StaticPageWriter<'input, I, A, W> 185 { 186 pub async fn run(mut self) -> Result<(), W::Error> { 187 while let Some(event) = self.context.next().await {
··· 180 } 181 } 182 183 + impl< 184 + 'input, 185 + I: Iterator<Item = Event<'input>>, 186 + A: AgentSession + IdentityResolver + 'input, 187 + W: StrWrite, 188 + > StaticPageWriter<'input, I, A, W> 189 { 190 pub async fn run(mut self) -> Result<(), W::Error> { 191 while let Some(event) = self.context.next().await {
+4 -4
crates/weaver-renderer/src/theme.rs
··· 30 subtle: CowStr::new_static("#908caa"), 31 emphasis: CowStr::new_static("#e0def4"), 32 primary: CowStr::new_static("#c4a7e7"), 33 - secondary: CowStr::new_static("#3e8fb0"), 34 - tertiary: CowStr::new_static("#9ccfd8"), 35 error: CowStr::new_static("#eb6f92"), 36 warning: CowStr::new_static("#f6c177"), 37 success: CowStr::new_static("#31748f"), ··· 45 pub fn default_fonts() -> ThemeFonts<'static> { 46 ThemeFonts { 47 body: CowStr::new_static( 48 - "IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 49 ), 50 heading: CowStr::new_static( 51 - "IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 52 ), 53 monospace: CowStr::new_static( 54 "'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace",
··· 30 subtle: CowStr::new_static("#908caa"), 31 emphasis: CowStr::new_static("#e0def4"), 32 primary: CowStr::new_static("#c4a7e7"), 33 + secondary: CowStr::new_static("#9ccfd8"), 34 + tertiary: CowStr::new_static("#ebbcba"), 35 error: CowStr::new_static("#eb6f92"), 36 warning: CowStr::new_static("#f6c177"), 37 success: CowStr::new_static("#31748f"), ··· 45 pub fn default_fonts() -> ThemeFonts<'static> { 46 ThemeFonts { 47 body: CowStr::new_static( 48 + "'IBM Plex', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 49 ), 50 heading: CowStr::new_static( 51 + "'IBM Plex Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 52 ), 53 monospace: CowStr::new_static( 54 "'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace",
crates/weaver-server/assets/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf

This is a binary file and will not be displayed.

crates/weaver-server/assets/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf

This is a binary file and will not be displayed.

+2 -2
crates/weaver-server/assets/styling/entry.css
··· 128 .entry-date { 129 margin-left: auto; 130 font-weight: 400; 131 - color: var(--color-muted); 132 } 133 134 .meta-label { 135 font-weight: 400; 136 - color: var(--color-muted); 137 } 138 139 .author-name {
··· 128 .entry-date { 129 margin-left: auto; 130 font-weight: 400; 131 + color: var(--color-subtle); 132 } 133 134 .meta-label { 135 font-weight: 400; 136 + color: var(--color-subtle); 137 } 138 139 .author-name {
+3
crates/weaver-server/src/main.rs
··· 121 rsx! { 122 document::Link { rel: "icon", href: FAVICON } 123 document::Link { rel: "stylesheet", href: MAIN_CSS } 124 Router::<Route> {} 125 } 126 }
··· 121 rsx! { 122 document::Link { rel: "icon", href: FAVICON } 123 document::Link { rel: "stylesheet", href: MAIN_CSS } 124 + document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Serif:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" } 125 + document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } 126 + document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" } 127 Router::<Route> {} 128 } 129 }
+5 -1
weaver_notes/Weaver - Long-form writing.md
··· 1 I grew up, like a lot of people on Bluesky, in the era of the internet where most of your online social interactions took place via text. I had a MySpace account, MSN messenger and Google Chat, I first got on Facebook back when they required a school email to sign up, I had a Tumblr, though not a LiveJournal. I was super into reddit for a long time. Big fan of Fanfiction.net and later Archive of Our Own. 2 3 > ![[weaver_photo_med.jpg]]*The namesake of what I'm building* ··· 33 34 >As to why I'm writing it in Rust (and currently zero Typescript) as opposed to Go and Typescript? Well it comes down to familiarity. Rust isn't necessarily anyone's first choice in a vacuum for a web-native programming language, but it works quite well as one, and I can share the vast majority of the protocol code, as well as the markdown rendering engine, between front and back end, with few if any compromises on performance, save a larger bundle size due to the nature of WebAssembly. And ultimately I may end up producing Typescript bindings for Jacquard and Weaver's core tools, if that's something people value, or I end up reconsidering doing web front-end in Rust. 35 ### Evolution 36 - Weaver is therefore very much an evolving thing. It will always have and support the proof-of-concept workflow as a first-class citizen. That's part of the benefit of building this on atproto. If I screw this up, not too hard for someone else to pick up the torch and continue.
··· 1 + ``` 2 + Or: "Get in kid, we're rebuilding the blogosphere!" 3 + ``` 4 + 5 I grew up, like a lot of people on Bluesky, in the era of the internet where most of your online social interactions took place via text. I had a MySpace account, MSN messenger and Google Chat, I first got on Facebook back when they required a school email to sign up, I had a Tumblr, though not a LiveJournal. I was super into reddit for a long time. Big fan of Fanfiction.net and later Archive of Our Own. 6 7 > ![[weaver_photo_med.jpg]]*The namesake of what I'm building* ··· 37 38 >As to why I'm writing it in Rust (and currently zero Typescript) as opposed to Go and Typescript? Well it comes down to familiarity. Rust isn't necessarily anyone's first choice in a vacuum for a web-native programming language, but it works quite well as one, and I can share the vast majority of the protocol code, as well as the markdown rendering engine, between front and back end, with few if any compromises on performance, save a larger bundle size due to the nature of WebAssembly. And ultimately I may end up producing Typescript bindings for Jacquard and Weaver's core tools, if that's something people value, or I end up reconsidering doing web front-end in Rust. 39 ### Evolution 40 + Weaver is therefore very much an evolving thing. It will always have and support the proof-of-concept workflow as a first-class citizen. That's part of the benefit of building this on atproto. If I screw this up, not too hard for someone else to pick up the torch and continue.