at main 450 lines 18 kB view raw
1//! Weaver renderer 2//! 3//! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS. 4//! 5 6use markdown_weaver::CowStr; 7use markdown_weaver::Event; 8use markdown_weaver::Tag; 9use n0_future::Stream; 10use yaml_rust2::Yaml; 11use yaml_rust2::YamlLoader; 12 13#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 14use regex::Regex; 15#[cfg(all(target_family = "wasm", target_os = "unknown"))] 16use regex_lite::Regex; 17 18use std::iter::Iterator; 19use std::ops::Range; 20use std::path::PathBuf; 21use std::pin::Pin; 22use std::sync::Arc; 23use std::sync::LazyLock; 24use std::sync::RwLock; 25use std::task::Poll; 26 27pub mod atproto; 28pub mod base_html; 29#[cfg(feature = "syntax-highlighting")] 30pub mod code_pretty; 31#[cfg(feature = "syntax-css")] 32pub mod css; 33pub mod facet; 34pub mod leaflet; 35pub mod math; 36#[cfg(feature = "pckt")] 37pub mod pckt; 38#[cfg(all(not(target_family = "wasm"), feature = "syntax-highlighting"))] 39pub mod static_site; 40pub mod theme; 41#[cfg(feature = "themes")] 42pub mod colour_gen; 43#[cfg(feature = "themes")] 44pub mod themes; 45pub mod types; 46pub mod utils; 47#[cfg(not(target_family = "wasm"))] 48pub mod walker; 49 50pub static OBSIDIAN_NOTE_LINK_RE: LazyLock<Regex> = LazyLock::new(|| { 51 Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap() 52}); 53 54#[derive(Debug, Default)] 55pub struct ContextIterator<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>> { 56 pub context: Option<EventContext>, 57 pub iter: I, 58 _phantom: std::marker::PhantomData<&'a ()>, 59} 60 61impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>> ContextIterator<'a, I> { 62 pub fn new(context: EventContext, iter: I) -> Self { 63 Self { 64 context: Some(context), 65 iter, 66 _phantom: std::marker::PhantomData, 67 } 68 } 69 70 pub fn default(iter: I) -> Self { 71 Self { 72 context: None, 73 iter, 74 _phantom: std::marker::PhantomData, 75 } 76 } 77} 78 79impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>> Iterator for ContextIterator<'a, I> { 80 type Item = (Event<'a>, Range<usize>, EventContext); 81 82 fn next(&mut self) -> Option<Self::Item> { 83 if let Some((next, range)) = self.iter.next() { 84 let ctxt = EventContext::get_context(&next, self.context.as_ref()); 85 self.context = Some(ctxt); 86 Some((next, range, ctxt)) 87 } else { 88 None 89 } 90 } 91} 92 93#[pin_project::pin_project] 94pub struct NotebookProcessor<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, CTX> { 95 context: CTX, 96 iter: ContextIterator<'a, I>, 97 #[pin] 98 pending_future: 99 Option<Pin<Box<dyn std::future::Future<Output = (Event<'a>, Range<usize>)> + 'a>>>, 100} 101 102impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, CTX> NotebookProcessor<'a, I, CTX> { 103 pub fn new(ctx: CTX, iter: ContextIterator<'a, I>) -> Self { 104 Self { 105 context: ctx, 106 iter, 107 pending_future: None, 108 } 109 } 110} 111 112impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, CTX: NotebookContext + Clone + 'a> Stream 113 for NotebookProcessor<'a, I, CTX> 114{ 115 type Item = (Event<'a>, Range<usize>); 116 117 fn size_hint(&self) -> (usize, Option<usize>) { 118 self.iter.size_hint() 119 } 120 fn poll_next( 121 mut self: Pin<&mut Self>, 122 cx: &mut std::task::Context<'_>, 123 ) -> Poll<Option<Self::Item>> { 124 // First, poll any pending future to completion 125 if let Some(fut) = self.as_mut().project().pending_future.as_mut().as_pin_mut() { 126 match fut.poll(cx) { 127 Poll::Ready(event_with_range) => { 128 // Clear the future and return the result 129 self.as_mut().project().pending_future.set(None); 130 return Poll::Ready(Some(event_with_range)); 131 } 132 Poll::Pending => { 133 // Keep the future for next poll 134 return Poll::Pending; 135 } 136 } 137 } 138 139 let mut this = self.project(); 140 let iter: &mut ContextIterator<'a, I> = this.iter; 141 if let Some((event, range, ctxt)) = iter.next() { 142 match ctxt { 143 EventContext::EmbedLink => match event { 144 Event::Start(ref tag) => match tag { 145 Tag::Embed { .. } => { 146 let tag_clone = tag.clone(); 147 let ctx_clone = this.context.clone(); 148 let range_clone = range.clone(); 149 let fut = async move { 150 let processed_tag = ctx_clone.handle_embed(tag_clone).await; 151 (Event::Start(processed_tag.into_static()), range_clone) 152 }; 153 154 *this.pending_future = Some(Box::pin(fut)); 155 156 if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 157 match fut.poll(cx) { 158 Poll::Ready(event_with_range) => { 159 *this.pending_future = None; 160 Poll::Ready(Some(event_with_range)) 161 } 162 Poll::Pending => Poll::Pending, 163 } 164 } else { 165 unreachable!() 166 } 167 } 168 _ => Poll::Ready(Some((event, range))), 169 }, 170 _ => Poll::Ready(Some((event, range))), 171 }, 172 EventContext::CodeBlock => Poll::Ready(Some((event, range))), 173 EventContext::Text => Poll::Ready(Some((event, range))), 174 EventContext::Html => Poll::Ready(Some((event, range))), 175 EventContext::Heading => Poll::Ready(Some((event, range))), 176 EventContext::Reference => match event { 177 Event::Start(ref tag) => match tag { 178 Tag::Link { .. } => { 179 let tag_clone = tag.clone(); 180 let ctx_clone = this.context.clone(); 181 let range_clone = range.clone(); 182 let fut = async move { 183 let processed_tag = ctx_clone.handle_link(tag_clone).await; 184 (Event::Start(processed_tag), range_clone) 185 }; 186 187 *this.pending_future = Some(Box::pin(fut)); 188 189 if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 190 match fut.poll(cx) { 191 Poll::Ready(event_with_range) => { 192 *this.pending_future = None; 193 Poll::Ready(Some(event_with_range)) 194 } 195 Poll::Pending => Poll::Pending, 196 } 197 } else { 198 unreachable!() 199 } 200 } 201 _ => Poll::Ready(Some((event, range))), 202 }, 203 Event::FootnoteReference(ref name) => { 204 this.context.handle_reference(name.clone()); 205 Poll::Ready(Some((event, range))) 206 } 207 _ => Poll::Ready(Some((event, range))), 208 }, 209 EventContext::RefDef => match event { 210 Event::Start(ref tag) => match tag { 211 Tag::FootnoteDefinition(name) => { 212 this.context.add_reference(name.clone()); 213 Poll::Ready(Some((event, range))) 214 } 215 _ => Poll::Ready(Some((event, range))), 216 }, 217 _ => Poll::Ready(Some((event, range))), 218 }, 219 EventContext::Link => match event { 220 Event::Start(ref tag) => match tag { 221 Tag::Link { .. } => { 222 let tag_clone = tag.clone(); 223 let ctx_clone = this.context.clone(); 224 let range_clone = range.clone(); 225 let fut = async move { 226 let processed_tag = ctx_clone.handle_link(tag_clone).await; 227 (Event::Start(processed_tag), range_clone) 228 }; 229 230 *this.pending_future = Some(Box::pin(fut)); 231 232 if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 233 match fut.poll(cx) { 234 Poll::Ready(event_with_range) => { 235 *this.pending_future = None; 236 Poll::Ready(Some(event_with_range)) 237 } 238 Poll::Pending => Poll::Pending, 239 } 240 } else { 241 unreachable!() 242 } 243 } 244 _ => Poll::Ready(Some((event, range))), 245 }, 246 _ => Poll::Ready(Some((event, range))), 247 }, 248 EventContext::Image => match event { 249 Event::Start(ref tag) => match tag { 250 Tag::Image { .. } => { 251 // Create future that handles the image and wraps result in Event::Start 252 let tag_clone = tag.clone(); 253 let ctx_clone = this.context.clone(); 254 let range_clone = range.clone(); 255 let fut = async move { 256 let processed_tag = ctx_clone.handle_image(tag_clone).await; 257 (Event::Start(processed_tag), range_clone) 258 }; 259 260 // Store the future and poll it 261 *this.pending_future = Some(Box::pin(fut)); 262 263 // Immediately poll the stored future 264 if let Some(fut) = this.pending_future.as_mut().as_pin_mut() { 265 match fut.poll(cx) { 266 Poll::Ready(event_with_range) => { 267 *this.pending_future = None; 268 Poll::Ready(Some(event_with_range)) 269 } 270 Poll::Pending => Poll::Pending, 271 } 272 } else { 273 unreachable!() 274 } 275 } 276 _ => Poll::Ready(Some((event, range))), 277 }, 278 _ => Poll::Ready(Some((event, range))), 279 }, 280 281 EventContext::Table => Poll::Ready(Some((event, range))), 282 EventContext::Metadata => match event { 283 Event::Text(ref text) => { 284 let frontmatter = Frontmatter::new(&text); 285 this.context.set_frontmatter(frontmatter); 286 Poll::Ready(Some((event, range))) 287 } 288 _ => Poll::Ready(Some((event, range))), 289 }, 290 EventContext::Other => Poll::Ready(Some((event, range))), 291 EventContext::None => Poll::Ready(Some((event, range))), 292 } 293 } else { 294 Poll::Ready(None) 295 } 296 } 297} 298 299pub trait NotebookContext { 300 fn set_entry_title(&self, title: CowStr<'_>); 301 fn entry_title(&self) -> CowStr<'_>; 302 fn normalized_entry_title(&self) -> CowStr<'_> { 303 let title = self.entry_title(); 304 let mut normalized = String::new(); 305 for c in title.chars() { 306 if c.is_ascii_alphanumeric() { 307 normalized.push(c); 308 } else if c.is_whitespace() && !normalized.is_empty() && !(c == '\n' || c == '\r') { 309 normalized.push('-'); 310 } else if c == '\n' { 311 normalized.push('_'); 312 } else if c == '\r' { 313 continue; 314 } else if !crate::utils::AVOID_URL_CHARS.contains(&c) { 315 normalized.push(c); 316 } 317 } 318 CowStr::Boxed(normalized.into_boxed_str()) 319 } 320 fn frontmatter(&self) -> Frontmatter; 321 fn set_frontmatter(&self, frontmatter: Frontmatter); 322 fn handle_link<'s>(&self, link: Tag<'s>) -> impl Future<Output = Tag<'s>>; 323 fn handle_image<'s>(&self, image: Tag<'s>) -> impl Future<Output = Tag<'s>>; 324 fn handle_embed<'s>(&self, embed: Tag<'s>) -> impl Future<Output = Tag<'s>>; 325 fn handle_reference(&self, reference: CowStr<'_>) -> CowStr<'_>; 326 fn add_reference(&self, reference: CowStr<'_>); 327} 328 329#[derive(Debug, Clone)] 330pub struct Frontmatter { 331 yaml: Arc<RwLock<Vec<Yaml>>>, 332} 333 334impl Frontmatter { 335 pub fn new(text: &str) -> Self { 336 let yaml = YamlLoader::load_from_str(text).unwrap_or_else(|_| vec![Yaml::BadValue]); 337 Self { 338 yaml: Arc::new(RwLock::new(yaml)), 339 } 340 } 341 342 pub fn contents(&self) -> Arc<RwLock<Vec<Yaml>>> { 343 self.yaml.clone() 344 } 345} 346 347impl Default for Frontmatter { 348 fn default() -> Self { 349 Frontmatter { 350 yaml: Arc::new(RwLock::new(vec![])), 351 } 352 } 353} 354 355#[derive(thiserror::Error, Debug, miette::Diagnostic)] 356pub enum RenderError { 357 #[error("WalkDir error at {}", path.display())] 358 #[diagnostic(code(crate::static_site::walker))] 359 WalkDirError { path: PathBuf, msg: String }, 360} 361 362#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 363pub enum EventContext { 364 EmbedLink, 365 CodeBlock, 366 #[default] 367 Text, 368 Html, 369 Heading, 370 Reference, 371 RefDef, 372 Image, 373 Link, 374 Table, 375 Metadata, 376 Other, 377 None, 378} 379 380impl EventContext { 381 pub fn get_context<'a>(event: &Event<'a>, prev: Option<&Self>) -> Self { 382 match event { 383 Event::Start(tag) => match tag { 384 Tag::Paragraph(_) => Self::Text, 385 Tag::Heading { .. } => Self::Heading, 386 Tag::BlockQuote(_block_quote_kind) => Self::Text, 387 Tag::CodeBlock(_code_block_kind) => Self::CodeBlock, 388 Tag::HtmlBlock => Self::Text, 389 Tag::List(_) => Self::Other, 390 Tag::Item => Self::Other, 391 Tag::FootnoteDefinition(_cow_str) => Self::RefDef, 392 Tag::DefinitionList => Self::Other, 393 Tag::DefinitionListTitle => Self::Other, 394 Tag::DefinitionListDefinition => Self::Other, 395 Tag::Table(_alignments) => Self::Table, 396 Tag::TableHead => Self::Table, 397 Tag::TableRow => Self::Table, 398 Tag::TableCell => Self::Table, 399 Tag::Emphasis => Self::Text, 400 Tag::Strong => Self::Text, 401 Tag::Strikethrough => Self::Text, 402 Tag::Superscript => Self::Text, 403 Tag::Subscript => Self::Text, 404 Tag::Link { .. } => Self::Link, 405 Tag::Image { .. } => Self::Image, 406 Tag::Embed { .. } => Self::EmbedLink, 407 Tag::WeaverBlock(_weaver_block_kind, _weaver_attributes) => Self::Metadata, 408 Tag::MetadataBlock(_metadata_block_kind) => Self::Metadata, 409 }, 410 Event::End(_tag_end) => Self::None, 411 Event::Text(_cow_str) => match prev { 412 Some(ctxt) => match ctxt { 413 EventContext::None => Self::Text, 414 _ => *ctxt, 415 }, 416 None => Self::Text, 417 }, 418 Event::Code(_cow_str) => Self::CodeBlock, 419 Event::InlineMath(_cow_str) => Self::Other, 420 Event::DisplayMath(_cow_str) => Self::Other, 421 Event::Html(_cow_str) => Self::Html, 422 Event::InlineHtml(_cow_str) => Self::Html, 423 Event::FootnoteReference(_cow_str) => Self::Reference, 424 Event::SoftBreak => Self::Other, 425 Event::HardBreak => Self::Other, 426 Event::Rule => Self::Other, 427 Event::TaskListMarker(_cow_str) => Self::Other, 428 Event::WeaverBlock(_cow_str) => Self::Other, 429 } 430 } 431 432 pub fn is_non_writing_block(&self) -> bool { 433 match self { 434 Self::Metadata => true, 435 _ => false, 436 } 437 } 438} 439 440pub fn default_md_options() -> markdown_weaver::Options { 441 markdown_weaver::Options::ENABLE_WIKILINKS 442 | markdown_weaver::Options::ENABLE_FOOTNOTES 443 | markdown_weaver::Options::ENABLE_TABLES 444 | markdown_weaver::Options::ENABLE_GFM 445 | markdown_weaver::Options::ENABLE_STRIKETHROUGH 446 | markdown_weaver::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS 447 | markdown_weaver::Options::ENABLE_OBSIDIAN_EMBEDS 448 | markdown_weaver::Options::ENABLE_MATH 449 | markdown_weaver::Options::ENABLE_HEADING_ATTRIBUTES 450}