at main 470 lines 18 kB view raw
1use crate::static_site::{StaticSiteOptions}; 2use crate::theme::ResolvedTheme; 3use crate::{Frontmatter, NotebookContext,default_md_options}; 4use dashmap::DashMap; 5use markdown_weaver::{CowStr, EmbedType, Tag, WeaverAttributes}; 6use std::{ 7 path::{Path, PathBuf}, 8 sync::Arc, 9}; 10use syntect::parsing::SyntaxSet; 11use weaver_common::{ 12 aturi_to_http, 13 jacquard::{ 14 client::{Agent, AgentSession, AgentSessionExt}, 15 prelude::*, 16 types::blob::MimeType, 17 }, 18}; 19use yaml_rust2::Yaml; 20 21#[derive(Debug, Clone)] 22pub enum KaTeXSource { 23 Cdn, 24 Local(PathBuf), 25} 26 27pub struct StaticSiteContext<A: AgentSession> { 28 pub options: StaticSiteOptions, 29 pub md_options: markdown_weaver::Options, 30 pub bsky_appview: CowStr<'static>, 31 pub root: PathBuf, 32 pub destination: PathBuf, 33 pub start_at: PathBuf, 34 pub frontmatter: Arc<DashMap<PathBuf, Frontmatter>>, 35 pub dir_contents: Option<Arc<[PathBuf]>>, 36 reference_map: Arc<DashMap<CowStr<'static>, PathBuf>>, 37 pub titles: Arc<DashMap<PathBuf, CowStr<'static>>>, 38 pub position: usize, 39 pub client: Option<reqwest::Client>, 40 agent: Option<Arc<Agent<A>>>, 41 42 pub theme: Option<Arc<ResolvedTheme<'static>>>, 43 pub katex_source: Option<KaTeXSource>, 44 pub syntax_set: Arc<SyntaxSet>, 45 pub index_file: Option<PathBuf>, 46} 47 48impl<A: AgentSession> Clone for StaticSiteContext<A> { 49 fn clone(&self) -> Self { 50 Self { 51 options: self.options.clone(), 52 md_options: self.md_options.clone(), 53 bsky_appview: self.bsky_appview.clone(), 54 root: self.root.clone(), 55 destination: self.destination.clone(), 56 start_at: self.start_at.clone(), 57 frontmatter: self.frontmatter.clone(), 58 dir_contents: self.dir_contents.clone(), 59 reference_map: self.reference_map.clone(), 60 titles: self.titles.clone(), 61 position: self.position.clone(), 62 client: self.client.clone(), 63 agent: self.agent.clone(), 64 theme: self.theme.clone(), 65 katex_source: self.katex_source.clone(), 66 syntax_set: self.syntax_set.clone(), 67 index_file: self.index_file.clone(), 68 } 69 } 70} 71 72impl<A: AgentSession> StaticSiteContext<A> { 73 pub fn clone_with_dir_contents(&self, dir_contents: &[PathBuf]) -> Self { 74 Self { 75 start_at: self.start_at.clone(), 76 root: self.root.clone(), 77 bsky_appview: self.bsky_appview.clone(), 78 options: self.options.clone(), 79 md_options: self.md_options.clone(), 80 frontmatter: self.frontmatter.clone(), 81 dir_contents: Some(Arc::from(dir_contents)), 82 destination: self.destination.clone(), 83 reference_map: self.reference_map.clone(), 84 titles: self.titles.clone(), 85 position: self.position, 86 client: self.client.clone(), 87 agent: self.agent.clone(), 88 theme: self.theme.clone(), 89 katex_source: self.katex_source.clone(), 90 syntax_set: self.syntax_set.clone(), 91 index_file: self.index_file.clone(), 92 } 93 } 94 95 pub fn clone_with_path(&self, path: impl AsRef<Path>) -> Self { 96 let position = if let Some(dir_contents) = &self.dir_contents { 97 dir_contents 98 .iter() 99 .position(|p| p == path.as_ref()) 100 .unwrap_or(0) 101 } else { 102 0 103 }; 104 Self { 105 start_at: self.start_at.clone(), 106 root: self.root.clone(), 107 bsky_appview: self.bsky_appview.clone(), 108 options: self.options.clone(), 109 md_options: self.md_options.clone(), 110 frontmatter: self.frontmatter.clone(), 111 dir_contents: self.dir_contents.clone(), 112 destination: self.destination.clone(), 113 reference_map: self.reference_map.clone(), 114 titles: self.titles.clone(), 115 position, 116 client: Some(reqwest::Client::default()), 117 agent: self.agent.clone(), 118 theme: self.theme.clone(), 119 katex_source: self.katex_source.clone(), 120 syntax_set: self.syntax_set.clone(), 121 index_file: self.index_file.clone(), 122 } 123 } 124 pub fn new(root: PathBuf, destination: PathBuf, session: Option<A>) -> Self { 125 use crate::theme::default_resolved_theme; 126 127 Self { 128 start_at: root.clone(), 129 root, 130 bsky_appview: CowStr::Borrowed("deer.social"), 131 options: StaticSiteOptions::default(), 132 md_options: default_md_options(), 133 frontmatter: Arc::new(DashMap::new()), 134 dir_contents: None, 135 destination, 136 reference_map: Arc::new(DashMap::new()), 137 titles: Arc::new(DashMap::new()), 138 position: 0, 139 client: Some(reqwest::Client::default()), 140 agent: session.map(|session| Arc::new(Agent::new(session))), 141 theme: Some(Arc::new(default_resolved_theme())), 142 katex_source: None, 143 syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()), 144 index_file: None, 145 } 146 } 147 148 pub fn with_theme(mut self, theme: ResolvedTheme<'static>) -> Self { 149 self.theme = Some(Arc::new(theme)); 150 self 151 } 152 153 pub fn current_path(&self) -> &PathBuf { 154 if let Some(dir_contents) = &self.dir_contents { 155 &dir_contents[self.position] 156 } else { 157 &self.start_at 158 } 159 } 160 161 #[inline] 162 pub fn handle_link_aturi<'s>(&self, link: Tag<'s>) -> Tag<'s> { 163 let link = crate::utils::resolve_at_ident_or_uri(&link, &self.bsky_appview); 164 self.handle_link_normal(link) 165 } 166 167 pub async fn handle_embed_aturi<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 168 match &embed { 169 Tag::Embed { 170 embed_type, 171 dest_url, 172 title, 173 id, 174 attrs, 175 } => { 176 if dest_url.starts_with("at://") { 177 let width = if let Some(attrs) = attrs { 178 let mut width = 600; 179 for attr in &attrs.attrs { 180 if attr.0 == CowStr::Borrowed("width".into()) { 181 width = attr.1.parse::<usize>().unwrap_or(600); 182 break; 183 } 184 } 185 width 186 } else { 187 600 188 }; 189 let html = if let Some(client) = &self.client { 190 if let Ok(resp) = client 191 .get("https://embed.bsky.app/oembed") 192 .query(&[ 193 ("url", dest_url.clone().into_string()), 194 ("maxwidth", width.to_string()), 195 ]) 196 .send() 197 .await 198 { 199 resp.text().await.ok() 200 } else { 201 None 202 } 203 } else { 204 None 205 }; 206 if let Some(html) = html { 207 let link = aturi_to_http(&dest_url, &self.bsky_appview) 208 .expect("assuming the at-uri is valid rn"); 209 let mut attrs = if let Some(attrs) = attrs { 210 attrs.clone() 211 } else { 212 WeaverAttributes { 213 classes: vec![], 214 attrs: vec![], 215 } 216 }; 217 attrs.attrs.push(("content".into(), html.into())); 218 Tag::Embed { 219 embed_type: EmbedType::Comments, // change this when i update markdown-weaver 220 dest_url: link.into_static(), 221 title: title.clone(), 222 id: id.clone(), 223 attrs: Some(attrs), 224 } 225 } else { 226 self.handle_embed_normal(embed).await 227 } 228 } else { 229 self.handle_embed_normal(embed).await 230 } 231 } 232 _ => embed, 233 } 234 } 235 236 pub async fn handle_embed_normal<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 237 // This option will REALLY slow down iteration over events. 238 if self.options.contains(StaticSiteOptions::INLINE_EMBEDS) { 239 match &embed { 240 Tag::Embed { 241 embed_type: _, 242 dest_url, 243 title, 244 id, 245 attrs, 246 } => { 247 let mut attrs = if let Some(attrs) = attrs { 248 attrs.clone() 249 } else { 250 WeaverAttributes { 251 classes: vec![], 252 attrs: vec![], 253 } 254 }; 255 let contents = if crate::utils::is_local_path(dest_url) { 256 let file_path = if crate::utils::is_relative_link(dest_url) { 257 let root_path = self.root.clone(); 258 root_path.join(Path::new(&dest_url as &str)) 259 } else { 260 PathBuf::from(&dest_url as &str) 261 }; 262 crate::utils::inline_file(&file_path).await 263 } else if let Some(client) = &self.client { 264 if let Ok(resp) = client.get(dest_url.clone().into_string()).send().await { 265 resp.text().await.ok() 266 } else { 267 None 268 } 269 } else { 270 None 271 }; 272 if let Some(contents) = contents { 273 attrs.attrs.push(("content".into(), contents.into())); 274 Tag::Embed { 275 embed_type: EmbedType::Markdown, // change this when i update markdown-weaver 276 dest_url: dest_url.clone(), 277 title: title.clone(), 278 id: id.clone(), 279 attrs: Some(attrs), 280 } 281 } else { 282 embed 283 } 284 } 285 _ => embed, 286 } 287 } else { 288 embed 289 } 290 } 291 292 /// This is a no-op for the static site renderer currently. 293 #[inline] 294 pub fn handle_link_normal<'s>(&self, link: Tag<'s>) -> Tag<'s> { 295 link 296 } 297 298 /// This is a no-op for the static site renderer currently. 299 #[inline] 300 pub fn handle_image_normal<'s>(&self, image: Tag<'s>) -> Tag<'s> { 301 image 302 } 303 304 pub fn set_options(&mut self, options: StaticSiteOptions) { 305 self.options = options; 306 } 307} 308 309impl<A: AgentSession + IdentityResolver> StaticSiteContext<A> { 310 /// TODO: rework this a bit, to not just do the same thing as whitewind 311 /// (also need to make a record to refer to them) that being said, doing 312 /// this with the static site renderer isn't *really* the standard workflow 313 pub async fn upload_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { 314 if let Some(agent) = &self.agent { 315 match &image { 316 Tag::Image { 317 link_type, 318 dest_url, 319 title, 320 id, 321 attrs, 322 } => { 323 if crate::utils::is_local_path(&dest_url) { 324 let root_path = self.root.clone(); 325 let file_path = root_path.join(Path::new(&dest_url as &str)); 326 if let Ok(image_data) = std::fs::read(&file_path) { 327 if let Ok(blob) = agent 328 .upload_blob(image_data, MimeType::new_static("image/jpg")) 329 .await 330 { 331 let (did, _) = agent.info().await.unwrap(); 332 let url = weaver_common::blob_url( 333 &did, 334 agent.endpoint().await.as_str(), 335 &blob.r#ref.0, 336 ); 337 return Tag::Image { 338 link_type: *link_type, 339 dest_url: url.into(), 340 title: title.clone(), 341 id: id.clone(), 342 attrs: attrs.clone(), 343 }; 344 } 345 } 346 } 347 } 348 _ => {} 349 } 350 } 351 image 352 } 353} 354 355impl<A: AgentSession + IdentityResolver> NotebookContext for StaticSiteContext<A> { 356 fn set_entry_title(&self, title: CowStr<'_>) { 357 let path = self.current_path(); 358 self.titles 359 .insert(path.clone(), title.clone().into_static()); 360 self.frontmatter.get_mut(path).map(|frontmatter| { 361 if let Ok(mut yaml) = frontmatter.yaml.write() { 362 if yaml.get(0).is_some_and(|y| y.is_hash()) { 363 let map = yaml.get_mut(0).unwrap().as_mut_hash().unwrap(); 364 map.insert( 365 Yaml::String("title".into()), 366 Yaml::String(title.into_static().into()), 367 ); 368 } 369 } 370 }); 371 } 372 fn entry_title(&self) -> CowStr<'_> { 373 let path = self.current_path(); 374 self.titles.get(path).unwrap().clone() 375 } 376 377 fn frontmatter(&self) -> Frontmatter { 378 let path = self.current_path(); 379 self.frontmatter.get(path).unwrap().value().clone() 380 } 381 382 fn set_frontmatter(&self, frontmatter: Frontmatter) { 383 let path = self.current_path(); 384 self.frontmatter.insert(path.clone(), frontmatter); 385 } 386 387 async fn handle_link<'s>(&self, link: Tag<'s>) -> Tag<'s> { 388 bitflags::bitflags_match!(self.options, { 389 // Split this somehow or just combine the options 390 StaticSiteOptions::RESOLVE_AT_URIS | StaticSiteOptions::RESOLVE_AT_IDENTIFIERS => { 391 self.handle_link_aturi(link) 392 } 393 _ => match &link { 394 Tag::Link { link_type, dest_url, title, id } => { 395 if self.options.contains(StaticSiteOptions::FLATTEN_STRUCTURE) { 396 let (parent, filename) = crate::utils::flatten_dir_to_just_one_parent(&dest_url); 397 let dest_url = if crate::utils::is_local_path(&dest_url) { 398 let filename = PathBuf::from(filename).with_extension("html"); 399 if crate::utils::is_relative_link(&dest_url) 400 && self.options.contains(StaticSiteOptions::CREATE_CHAPTERS_BY_DIRECTORY) { 401 if !parent.is_empty() { 402 CowStr::Boxed(format!("./{}/{}", parent, filename.display()).into_boxed_str()) 403 } else { 404 CowStr::Boxed(format!("./{}", filename.display()).into_boxed_str()) 405 } 406 } else { 407 CowStr::Boxed(format!("./entry/{}", filename.display()).into_boxed_str()) 408 } 409 } else { 410 dest_url.clone() 411 }; 412 Tag::Link { 413 link_type: *link_type, 414 dest_url, 415 title: title.clone(), 416 id: id.clone(), 417 } 418 } else { 419 if crate::utils::is_local_path(&dest_url) { 420 let filename = PathBuf::from(dest_url.as_ref() as &str).with_extension("html"); 421 Tag::Link { 422 link_type: *link_type, 423 dest_url: CowStr::Boxed(filename.to_string_lossy().into()), 424 title: title.clone(), 425 id: id.clone(), 426 } 427 } else { 428 link 429 } 430 } 431 }, 432 _ => link, 433 } 434 }) 435 } 436 437 async fn handle_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { 438 if self.options.contains(StaticSiteOptions::UPLOAD_BLOBS) { 439 self.upload_image(image).await 440 } else { 441 self.handle_image_normal(image) 442 } 443 } 444 445 async fn handle_embed<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 446 if self.options.contains(StaticSiteOptions::RESOLVE_AT_URIS) 447 || self.options.contains(StaticSiteOptions::ADD_LINK_PREVIEWS) 448 { 449 self.handle_embed_aturi(embed).await 450 } else { 451 self.handle_embed_normal(embed).await 452 } 453 } 454 455 fn handle_reference(&self, reference: CowStr<'_>) -> CowStr<'_> { 456 let reference = reference.into_static(); 457 if let Some(reference) = self.reference_map.get(&reference) { 458 let path = reference.value().clone(); 459 CowStr::Boxed(path.to_string_lossy().into_owned().into_boxed_str()) 460 } else { 461 reference 462 } 463 } 464 465 fn add_reference(&self, reference: CowStr<'_>) { 466 let path = self.current_path(); 467 self.reference_map 468 .insert(reference.into_static(), path.clone()); 469 } 470}