at main 1115 lines 45 kB view raw
1use crate::{NotebookProcessor, base_html::TableState, static_site::context::StaticSiteContext}; 2use dashmap::DashMap; 3use markdown_weaver::{ 4 Alignment, BlockQuoteKind, CodeBlockKind, CowStr, EmbedType, Event, LinkType, 5 ParagraphContext, Tag, WeaverAttributes, 6}; 7use markdown_weaver_escape::{StrWrite, escape_href, escape_html, escape_html_body_text}; 8use n0_future::StreamExt; 9use std::ops::Range; 10use weaver_common::jacquard::{client::AgentSession, prelude::*}; 11 12/// Tracks the type of wrapper element emitted for WeaverBlock prefix 13#[derive(Debug, Clone, Copy, PartialEq, Eq)] 14enum WrapperElement { 15 Aside, 16 Div, 17} 18 19pub struct StaticPageWriter<'input, I: Iterator<Item = (Event<'input>, Range<usize>)>, A: AgentSession, W: StrWrite> 20{ 21 context: NotebookProcessor<'input, I, StaticSiteContext<A>>, 22 writer: W, 23 /// Source text for gap detection 24 source: &'input str, 25 /// Whether or not the last write wrote a newline. 26 end_newline: bool, 27 28 /// Whether if inside a metadata block (text should not be written) 29 in_non_writing_block: bool, 30 31 table_state: TableState, 32 table_alignments: Vec<Alignment>, 33 table_cell_index: usize, 34 numbers: DashMap<CowStr<'input>, usize>, 35 36 code_buffer: Option<(Option<String>, String)>, // (lang, content) 37 38 /// Pending WeaverBlock attrs to apply to the next block element 39 pending_block_attrs: Option<WeaverAttributes<'static>>, 40 /// Type of wrapper element currently open, and the block depth at which it was opened 41 active_wrapper: Option<(WrapperElement, usize)>, 42 /// Current block nesting depth (for wrapper close tracking) 43 block_depth: usize, 44 /// Buffer for WeaverBlock text content (to parse for attrs) 45 weaver_block_buffer: String, 46 /// Pending footnote reference waiting to see if definition follows immediately 47 pending_footnote: Option<(CowStr<'static>, usize)>, 48 /// Buffer for content between footnote ref and resolution 49 pending_footnote_content: String, 50 /// Whether current footnote definition is being rendered as a sidenote 51 in_sidenote: bool, 52 /// Whether we're deferring paragraph close for sidenote handling 53 defer_paragraph_close: bool, 54 /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 55 pending_paragraph_open: Option<String>, 56 /// Byte offset where last sidenote ended (for gap detection) 57 sidenote_end_offset: Option<usize>, 58} 59 60impl<'input, I: Iterator<Item = (Event<'input>, Range<usize>)>, A: AgentSession, W: StrWrite> 61 StaticPageWriter<'input, I, A, W> 62{ 63 pub fn new( 64 context: NotebookProcessor<'input, I, StaticSiteContext<A>>, 65 writer: W, 66 source: &'input str, 67 ) -> Self { 68 Self { 69 context, 70 writer, 71 source, 72 end_newline: true, 73 in_non_writing_block: false, 74 table_state: TableState::Head, 75 table_alignments: vec![], 76 table_cell_index: 0, 77 numbers: DashMap::new(), 78 code_buffer: None, 79 pending_block_attrs: None, 80 active_wrapper: None, 81 block_depth: 0, 82 weaver_block_buffer: String::new(), 83 pending_footnote: None, 84 pending_footnote_content: String::new(), 85 in_sidenote: false, 86 defer_paragraph_close: false, 87 pending_paragraph_open: None, 88 sidenote_end_offset: None, 89 } 90 } 91 92 /// Parse WeaverBlock text content into attributes. 93 /// Format: comma-separated, colon for key:value, otherwise class. 94 /// Example: ".aside, width: 300px" -> classes: ["aside"], attrs: [("width", "300px")] 95 fn parse_weaver_attrs(text: &str) -> WeaverAttributes<'static> { 96 let mut classes = Vec::new(); 97 let mut attrs = Vec::new(); 98 99 for part in text.split(',') { 100 let part = part.trim(); 101 if part.is_empty() { 102 continue; 103 } 104 105 if let Some((key, value)) = part.split_once(':') { 106 let key = key.trim(); 107 let value = value.trim(); 108 if !key.is_empty() && !value.is_empty() { 109 attrs.push(( 110 CowStr::from(key.to_string()), 111 CowStr::from(value.to_string()), 112 )); 113 } 114 } else { 115 // No colon - treat as class, strip leading dot if present 116 let class = part.strip_prefix('.').unwrap_or(part); 117 if !class.is_empty() { 118 classes.push(CowStr::from(class.to_string())); 119 } 120 } 121 } 122 123 WeaverAttributes { classes, attrs } 124 } 125 126 /// Writes a new line. 127 #[inline] 128 fn write_newline(&mut self) -> Result<(), W::Error> { 129 self.end_newline = true; 130 self.writer.write_str("\n") 131 } 132 133 /// Writes a buffer, and tracks whether or not a newline was written. 134 #[inline] 135 fn write(&mut self, s: &str) -> Result<(), W::Error> { 136 self.writer.write_str(s)?; 137 138 if !s.is_empty() { 139 self.end_newline = s.ends_with('\n'); 140 } 141 Ok(()) 142 } 143 144 /// Close deferred paragraph if we're in that state. 145 /// Called when a non-paragraph block element starts. 146 fn close_deferred_paragraph(&mut self) -> Result<(), W::Error> { 147 // Also flush any pending paragraph open (shouldn't happen in normal flow, but be defensive) 148 if let Some(opening) = self.pending_paragraph_open.take() { 149 self.write(&opening)?; 150 self.write(">")?; 151 } 152 if self.defer_paragraph_close { 153 // Flush pending footnote as traditional before closing 154 self.flush_pending_footnote()?; 155 self.write("</p>\n")?; 156 self.block_depth -= 1; 157 self.close_wrapper()?; 158 self.defer_paragraph_close = false; 159 } 160 Ok(()) 161 } 162 163 /// Flush any pending footnote reference as a traditional footnote, 164 /// then write any buffered content that came after the reference. 165 fn flush_pending_footnote(&mut self) -> Result<(), W::Error> { 166 if let Some((name, number)) = self.pending_footnote.take() { 167 // Emit traditional footnote reference 168 self.write("<sup class=\"footnote-reference\"><a href=\"#")?; 169 escape_html(&mut self.writer, &name)?; 170 self.write("\">")?; 171 write!(&mut self.writer, "{}", number)?; 172 self.write("</a></sup>")?; 173 // Write any buffered content 174 if !self.pending_footnote_content.is_empty() { 175 let content = std::mem::take(&mut self.pending_footnote_content); 176 escape_html_body_text(&mut self.writer, &content)?; 177 self.end_newline = content.ends_with('\n'); 178 } 179 } 180 Ok(()) 181 } 182 183 /// Emit wrapper element start based on pending block attrs 184 /// Returns true if a wrapper was emitted 185 fn emit_wrapper_start(&mut self) -> Result<bool, W::Error> { 186 if let Some(attrs) = self.pending_block_attrs.take() { 187 let is_aside = attrs.classes.iter().any(|c| c.as_ref() == "aside"); 188 189 if !self.end_newline { 190 self.write("\n")?; 191 } 192 193 if is_aside { 194 self.write("<aside")?; 195 self.active_wrapper = Some((WrapperElement::Aside, self.block_depth)); 196 } else { 197 self.write("<div")?; 198 self.active_wrapper = Some((WrapperElement::Div, self.block_depth)); 199 } 200 201 // Write classes (excluding "aside" if using <aside> element) 202 let classes: Vec<_> = if is_aside { 203 attrs 204 .classes 205 .iter() 206 .filter(|c| c.as_ref() != "aside") 207 .collect() 208 } else { 209 attrs.classes.iter().collect() 210 }; 211 212 if !classes.is_empty() { 213 self.write(" class=\"")?; 214 for (i, class) in classes.iter().enumerate() { 215 if i > 0 { 216 self.write(" ")?; 217 } 218 escape_html(&mut self.writer, class)?; 219 } 220 self.write("\"")?; 221 } 222 223 // Write other attrs 224 for (attr, value) in &attrs.attrs { 225 self.write(" ")?; 226 escape_html(&mut self.writer, attr)?; 227 self.write("=\"")?; 228 escape_html(&mut self.writer, value)?; 229 self.write("\"")?; 230 } 231 232 self.write(">\n")?; 233 Ok(true) 234 } else { 235 Ok(false) 236 } 237 } 238 239 /// Close active wrapper element if one is open and we're at the right depth 240 fn close_wrapper(&mut self) -> Result<(), W::Error> { 241 if let Some((wrapper, open_depth)) = self.active_wrapper.take() { 242 if self.block_depth == open_depth { 243 match wrapper { 244 WrapperElement::Aside => self.write("</aside>\n")?, 245 WrapperElement::Div => self.write("</div>\n")?, 246 } 247 } else { 248 // Not at the right depth yet, put it back 249 self.active_wrapper = Some((wrapper, open_depth)); 250 } 251 } 252 Ok(()) 253 } 254 255 fn end_tag(&mut self, tag: markdown_weaver::TagEnd, range: Range<usize>) -> Result<(), W::Error> { 256 use markdown_weaver::TagEnd; 257 match tag { 258 TagEnd::HtmlBlock => {} 259 TagEnd::Paragraph(ctx) => { 260 if self.in_sidenote { 261 // Inside sidenote span - don't emit paragraph tags 262 } else if ctx == ParagraphContext::Interrupted && self.pending_footnote.is_some() { 263 // Paragraph was interrupted AND we have a pending footnote, 264 // defer the </p> close - the sidenote will be rendered inline 265 self.defer_paragraph_close = true; 266 // Don't decrement block_depth yet - we're continuing the virtual paragraph 267 } else if self.defer_paragraph_close { 268 // We were deferring but now closing for real 269 self.write("</p>\n")?; 270 self.block_depth -= 1; 271 self.close_wrapper()?; 272 self.defer_paragraph_close = false; 273 } else { 274 // Flush any pending paragraph open (for empty paragraphs) 275 if let Some(opening) = self.pending_paragraph_open.take() { 276 self.write(&opening)?; 277 self.write(">")?; 278 } 279 self.write("</p>\n")?; 280 self.block_depth -= 1; 281 self.close_wrapper()?; 282 } 283 } 284 TagEnd::Heading(level) => { 285 self.write("</")?; 286 write!(&mut self.writer, "{}", level)?; 287 self.block_depth -= 1; 288 // Don't close wrapper - headings typically go with following block 289 self.write(">\n")?; 290 } 291 TagEnd::Table => { 292 self.write("</tbody></table>\n")?; 293 self.block_depth -= 1; 294 self.close_wrapper()?; 295 } 296 TagEnd::TableHead => { 297 self.write("</tr></thead><tbody>\n")?; 298 self.table_state = TableState::Body; 299 } 300 TagEnd::TableRow => { 301 self.write("</tr>\n")?; 302 } 303 TagEnd::TableCell => { 304 match self.table_state { 305 TableState::Head => { 306 self.write("</th>")?; 307 } 308 TableState::Body => { 309 self.write("</td>")?; 310 } 311 } 312 self.table_cell_index += 1; 313 } 314 TagEnd::BlockQuote(_) => { 315 // Close any deferred paragraph before closing blockquote 316 // (footnotes inside blockquotes can't be sidenotes since def is outside) 317 self.close_deferred_paragraph()?; 318 self.write("</blockquote>\n")?; 319 self.block_depth -= 1; 320 self.close_wrapper()?; 321 } 322 TagEnd::CodeBlock => { 323 if let Some((lang, buffer)) = self.code_buffer.take() { 324 if let Some(ref lang_str) = lang { 325 // Use a temporary String buffer for syntect 326 let mut temp_output = String::new(); 327 match crate::code_pretty::highlight( 328 &self.context.context.syntax_set, 329 Some(lang_str), 330 &buffer, 331 &mut temp_output, 332 ) { 333 Ok(_) => { 334 self.write(&temp_output)?; 335 } 336 Err(_) => { 337 // Fallback to plain code block 338 self.write("<pre><code class=\"language-")?; 339 escape_html(&mut self.writer, lang_str)?; 340 self.write("\">")?; 341 escape_html_body_text(&mut self.writer, &buffer)?; 342 self.write("</code></pre>\n")?; 343 } 344 } 345 } else { 346 self.write("<pre><code>")?; 347 escape_html_body_text(&mut self.writer, &buffer)?; 348 self.write("</code></pre>\n")?; 349 } 350 } else { 351 self.write("</code></pre>\n")?; 352 } 353 self.block_depth -= 1; 354 self.close_wrapper()?; 355 } 356 TagEnd::List(true) => { 357 self.write("</ol>\n")?; 358 self.block_depth -= 1; 359 self.close_wrapper()?; 360 } 361 TagEnd::List(false) => { 362 self.write("</ul>\n")?; 363 self.block_depth -= 1; 364 self.close_wrapper()?; 365 } 366 TagEnd::Item => { 367 self.write("</li>\n")?; 368 } 369 TagEnd::DefinitionList => { 370 self.write("</dl>\n")?; 371 self.block_depth -= 1; 372 self.close_wrapper()?; 373 } 374 TagEnd::DefinitionListTitle => { 375 self.write("</dt>\n")?; 376 } 377 TagEnd::DefinitionListDefinition => { 378 self.write("</dd>\n")?; 379 } 380 TagEnd::Emphasis => { 381 self.write("</em>")?; 382 } 383 TagEnd::Superscript => { 384 self.write("</sup>")?; 385 } 386 TagEnd::Subscript => { 387 self.write("</sub>")?; 388 } 389 TagEnd::Strong => { 390 self.write("</strong>")?; 391 } 392 TagEnd::Strikethrough => { 393 self.write("</del>")?; 394 } 395 TagEnd::Link => { 396 self.write("</a>")?; 397 } 398 TagEnd::Image => (), // shouldn't happen, handled in start 399 TagEnd::Embed => (), // shouldn't happen, handled in start 400 TagEnd::WeaverBlock(_) => { 401 self.in_non_writing_block = false; 402 eprintln!( 403 "[TagEnd::WeaverBlock] buffer: {:?}", 404 self.weaver_block_buffer 405 ); 406 // Parse the buffered text for attrs and store for next block 407 if !self.weaver_block_buffer.is_empty() { 408 let parsed = Self::parse_weaver_attrs(&self.weaver_block_buffer); 409 eprintln!("[TagEnd::WeaverBlock] parsed: {:?}", parsed); 410 self.weaver_block_buffer.clear(); 411 // Merge with any existing pending attrs or set new 412 if let Some(ref mut existing) = self.pending_block_attrs { 413 existing.classes.extend(parsed.classes); 414 existing.attrs.extend(parsed.attrs); 415 } else { 416 self.pending_block_attrs = Some(parsed); 417 } 418 eprintln!( 419 "[TagEnd::WeaverBlock] pending_block_attrs now: {:?}", 420 self.pending_block_attrs 421 ); 422 } 423 } 424 TagEnd::FootnoteDefinition => { 425 if self.in_sidenote { 426 self.write("</span>")?; 427 self.in_sidenote = false; 428 // Record where sidenote ended for gap detection 429 self.sidenote_end_offset = Some(range.end); 430 // Write any buffered content that came after the ref 431 if !self.pending_footnote_content.is_empty() { 432 let content = std::mem::take(&mut self.pending_footnote_content); 433 escape_html_body_text(&mut self.writer, &content)?; 434 self.end_newline = content.ends_with('\n'); 435 } 436 } else { 437 self.write("</div>\n")?; 438 } 439 } 440 TagEnd::MetadataBlock(_) => { 441 self.in_non_writing_block = false; 442 } 443 } 444 Ok(()) 445 } 446} 447 448impl< 449 'input, 450 I: Iterator<Item = (Event<'input>, Range<usize>)>, 451 A: AgentSession + IdentityResolver + 'input, 452 W: StrWrite, 453> StaticPageWriter<'input, I, A, W> 454{ 455 pub async fn run(mut self) -> Result<(), W::Error> { 456 while let Some((event, range)) = self.context.next().await { 457 self.process_event(event, range).await? 458 } 459 self.finalize() 460 } 461 462 /// Finalize output, closing any deferred state 463 fn finalize(&mut self) -> Result<(), W::Error> { 464 // Flush any pending footnote as traditional 465 self.flush_pending_footnote()?; 466 // Close deferred paragraph if any 467 if self.defer_paragraph_close { 468 self.write("</p>\n")?; 469 self.block_depth -= 1; 470 self.close_wrapper()?; 471 self.defer_paragraph_close = false; 472 } 473 Ok(()) 474 } 475 476 async fn process_event(&mut self, event: Event<'input>, range: Range<usize>) -> Result<(), W::Error> { 477 use markdown_weaver::Event::*; 478 match event { 479 Start(tag) => { 480 println!("Start tag: {:?}", tag); 481 self.start_tag(tag, range).await?; 482 } 483 End(tag) => { 484 self.end_tag(tag, range)?; 485 } 486 Text(text) => { 487 // If buffering code, append to buffer instead of writing 488 if let Some((_, ref mut buffer)) = self.code_buffer { 489 buffer.push_str(&text); 490 } else if self.pending_footnote.is_some() { 491 // Buffer text while waiting to see if footnote def follows 492 self.pending_footnote_content.push_str(&text); 493 } else if !self.in_non_writing_block { 494 // Flush pending paragraph with dir attribute if needed 495 if let Some(opening) = self.pending_paragraph_open.take() { 496 if let Some(dir) = crate::utils::detect_text_direction(&text) { 497 self.write(&opening)?; 498 self.write(" dir=\"")?; 499 self.write(dir)?; 500 self.write("\">")?; 501 } else { 502 self.write(&opening)?; 503 self.write(">")?; 504 } 505 } 506 escape_html_body_text(&mut self.writer, &text)?; 507 self.end_newline = text.ends_with('\n'); 508 } 509 } 510 Code(text) => { 511 self.write("<code>")?; 512 escape_html_body_text(&mut self.writer, &text)?; 513 self.write("</code>")?; 514 } 515 InlineMath(text) => { 516 self.write(r#"<span class="math math-inline">"#)?; 517 escape_html(&mut self.writer, &text)?; 518 self.write("</span>")?; 519 } 520 DisplayMath(text) => { 521 self.write(r#"<span class="math math-display">"#)?; 522 escape_html(&mut self.writer, &text)?; 523 self.write("</span>")?; 524 } 525 Html(html) | InlineHtml(html) => { 526 self.write(&html)?; 527 } 528 SoftBreak => { 529 if self.pending_footnote.is_some() { 530 self.pending_footnote_content.push('\n'); 531 } else { 532 self.write_newline()?; 533 } 534 } 535 HardBreak => { 536 if self.pending_footnote.is_some() { 537 self.pending_footnote_content.push_str("<br />\n"); 538 } else { 539 self.write("<br />\n")?; 540 } 541 } 542 Rule => { 543 if self.end_newline { 544 self.write("<hr />\n")?; 545 } else { 546 self.write("\n<hr />\n")?; 547 } 548 } 549 FootnoteReference(name) => { 550 // Flush any existing pending footnote as traditional 551 self.flush_pending_footnote()?; 552 // Get/create footnote number 553 let len = self.numbers.len() + 1; 554 let number = *self 555 .numbers 556 .entry(name.clone().into_static()) 557 .or_insert(len); 558 // Buffer this reference to see if definition follows immediately 559 self.pending_footnote = Some((name.into_static(), number)); 560 } 561 TaskListMarker(true) => { 562 self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\" aria-label=\"Completed task\"/>\n")?; 563 } 564 TaskListMarker(false) => { 565 self.write("<input disabled=\"\" type=\"checkbox\" aria-label=\"Incomplete task\"/>\n")?; 566 } 567 WeaverBlock(text) => { 568 // Buffer WeaverBlock content for parsing on End 569 eprintln!("[WeaverBlock event] text: {:?}", text); 570 self.weaver_block_buffer.push_str(&text); 571 } 572 } 573 Ok(()) 574 } 575 576 // run raw text, consuming end tag 577 async fn raw_text(&mut self) -> Result<(), W::Error> { 578 use markdown_weaver::Event::*; 579 let mut nest = 0; 580 while let Some((event, _range)) = self.context.next().await { 581 match event { 582 Start(_) => nest += 1, 583 End(_) => { 584 if nest == 0 { 585 break; 586 } 587 nest -= 1; 588 } 589 Html(_) => {} 590 InlineHtml(text) | Code(text) | Text(text) => { 591 // Don't use escape_html_body_text here. 592 // The output of this function is used in the `alt` attribute. 593 escape_html(&mut self.writer, &text)?; 594 self.end_newline = text.ends_with('\n'); 595 } 596 InlineMath(text) => { 597 self.write("$")?; 598 escape_html(&mut self.writer, &text)?; 599 self.write("$")?; 600 } 601 DisplayMath(text) => { 602 self.write("$$")?; 603 escape_html(&mut self.writer, &text)?; 604 self.write("$$")?; 605 } 606 SoftBreak | HardBreak | Rule => { 607 self.write(" ")?; 608 } 609 FootnoteReference(name) => { 610 let len = self.numbers.len() + 1; 611 let number = *self.numbers.entry(name.into_static()).or_insert(len); 612 write!(&mut self.writer, "[{}]", number)?; 613 } 614 TaskListMarker(true) => self.write("[x]")?, 615 TaskListMarker(false) => self.write("[ ]")?, 616 WeaverBlock(_) => { 617 println!("Weaver block internal"); 618 } 619 } 620 } 621 Ok(()) 622 } 623 624 /// Writes the start of an HTML tag. 625 async fn start_tag(&mut self, tag: Tag<'input>, range: Range<usize>) -> Result<(), W::Error> { 626 /// Minimum gap size that indicates a paragraph break (represents \n\n) 627 const MIN_PARAGRAPH_GAP: usize = 2; 628 629 match tag { 630 Tag::HtmlBlock => Ok(()), 631 Tag::Paragraph(_) => { 632 if self.in_sidenote { 633 // Inside sidenote span - don't emit paragraph tags 634 Ok(()) 635 } else if self.defer_paragraph_close { 636 // Check gap size to decide whether to continue or start new paragraph 637 if let Some(sidenote_end) = self.sidenote_end_offset.take() { 638 let gap = range.start.saturating_sub(sidenote_end); 639 if gap > MIN_PARAGRAPH_GAP { 640 // Large gap - close deferred paragraph and start new one 641 self.write("</p>\n")?; 642 self.block_depth -= 1; 643 self.close_wrapper()?; 644 self.defer_paragraph_close = false; 645 // Now start the new paragraph normally 646 self.flush_pending_footnote()?; 647 self.emit_wrapper_start()?; 648 self.block_depth += 1; 649 let opening = if self.end_newline { 650 String::from("<p") 651 } else { 652 String::from("\n<p") 653 }; 654 self.pending_paragraph_open = Some(opening); 655 } else { 656 // Small gap - continue same paragraph, just clear defer flag 657 self.defer_paragraph_close = false; 658 } 659 } else { 660 // No sidenote offset recorded, fall back to old behavior 661 self.defer_paragraph_close = false; 662 } 663 Ok(()) 664 } else { 665 self.flush_pending_footnote()?; 666 self.emit_wrapper_start()?; 667 self.block_depth += 1; 668 // Buffer paragraph opening for dir attribute detection 669 let opening = if self.end_newline { 670 String::from("<p") 671 } else { 672 String::from("\n<p") 673 }; 674 self.pending_paragraph_open = Some(opening); 675 Ok(()) 676 } 677 } 678 Tag::Heading { 679 level, 680 id, 681 classes, 682 attrs, 683 } => { 684 self.close_deferred_paragraph()?; 685 self.emit_wrapper_start()?; 686 self.block_depth += 1; 687 if self.end_newline { 688 self.write("<")?; 689 } else { 690 self.write("\n<")?; 691 } 692 write!(&mut self.writer, "{}", level)?; 693 if let Some(id) = id { 694 self.write(" id=\"")?; 695 escape_html(&mut self.writer, &id)?; 696 self.write("\"")?; 697 } 698 let mut classes = classes.iter(); 699 if let Some(class) = classes.next() { 700 self.write(" class=\"")?; 701 escape_html(&mut self.writer, class)?; 702 for class in classes { 703 self.write(" ")?; 704 escape_html(&mut self.writer, class)?; 705 } 706 self.write("\"")?; 707 } 708 for (attr, value) in attrs { 709 self.write(" ")?; 710 escape_html(&mut self.writer, &attr)?; 711 if let Some(val) = value { 712 self.write("=\"")?; 713 escape_html(&mut self.writer, &val)?; 714 self.write("\"")?; 715 } else { 716 self.write("=\"\"")?; 717 } 718 } 719 self.write(">") 720 } 721 Tag::Table(alignments) => { 722 self.close_deferred_paragraph()?; 723 self.emit_wrapper_start()?; 724 self.block_depth += 1; 725 self.table_alignments = alignments; 726 self.write("<table>") 727 } 728 Tag::TableHead => { 729 self.table_state = TableState::Head; 730 self.table_cell_index = 0; 731 self.write("<thead><tr>") 732 } 733 Tag::TableRow => { 734 self.table_cell_index = 0; 735 self.write("<tr>") 736 } 737 Tag::TableCell => { 738 match self.table_state { 739 TableState::Head => { 740 self.write("<th")?; 741 } 742 TableState::Body => { 743 self.write("<td")?; 744 } 745 } 746 match self.table_alignments.get(self.table_cell_index) { 747 Some(&Alignment::Left) => self.write(" style=\"text-align: left\">"), 748 Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"), 749 Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"), 750 _ => self.write(">"), 751 } 752 } 753 Tag::BlockQuote(kind) => { 754 self.close_deferred_paragraph()?; 755 self.emit_wrapper_start()?; 756 self.block_depth += 1; 757 let class_str = match kind { 758 None => "", 759 Some(kind) => match kind { 760 BlockQuoteKind::Note => " class=\"markdown-alert-note\"", 761 BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"", 762 BlockQuoteKind::Important => " class=\"markdown-alert-important\"", 763 BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"", 764 BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"", 765 }, 766 }; 767 if self.end_newline { 768 self.write(&format!("<blockquote{}>\n", class_str)) 769 } else { 770 self.write(&format!("\n<blockquote{}>\n", class_str)) 771 } 772 } 773 Tag::CodeBlock(info) => { 774 self.close_deferred_paragraph()?; 775 self.emit_wrapper_start()?; 776 self.block_depth += 1; 777 if !self.end_newline { 778 self.write_newline()?; 779 } 780 match info { 781 CodeBlockKind::Fenced(info) => { 782 let lang = info.split(' ').next().unwrap(); 783 let lang_opt = if lang.is_empty() { 784 None 785 } else { 786 Some(lang.to_string()) 787 }; 788 // Start buffering 789 self.code_buffer = Some((lang_opt, String::new())); 790 Ok(()) 791 } 792 CodeBlockKind::Indented => { 793 // Start buffering with no language 794 self.code_buffer = Some((None, String::new())); 795 Ok(()) 796 } 797 } 798 } 799 Tag::List(Some(1)) => { 800 self.close_deferred_paragraph()?; 801 self.emit_wrapper_start()?; 802 self.block_depth += 1; 803 if self.end_newline { 804 self.write("<ol>\n") 805 } else { 806 self.write("\n<ol>\n") 807 } 808 } 809 Tag::List(Some(start)) => { 810 self.close_deferred_paragraph()?; 811 self.emit_wrapper_start()?; 812 self.block_depth += 1; 813 if self.end_newline { 814 self.write("<ol start=\"")?; 815 } else { 816 self.write("\n<ol start=\"")?; 817 } 818 write!(&mut self.writer, "{}", start)?; 819 self.write("\">\n") 820 } 821 Tag::List(None) => { 822 self.close_deferred_paragraph()?; 823 self.emit_wrapper_start()?; 824 self.block_depth += 1; 825 if self.end_newline { 826 self.write("<ul>\n") 827 } else { 828 self.write("\n<ul>\n") 829 } 830 } 831 Tag::Item => { 832 if self.end_newline { 833 self.write("<li>") 834 } else { 835 self.write("\n<li>") 836 } 837 } 838 Tag::DefinitionList => { 839 self.close_deferred_paragraph()?; 840 self.emit_wrapper_start()?; 841 self.block_depth += 1; 842 if self.end_newline { 843 self.write("<dl>\n") 844 } else { 845 self.write("\n<dl>\n") 846 } 847 } 848 Tag::DefinitionListTitle => { 849 if self.end_newline { 850 self.write("<dt>") 851 } else { 852 self.write("\n<dt>") 853 } 854 } 855 Tag::DefinitionListDefinition => { 856 if self.end_newline { 857 self.write("<dd>") 858 } else { 859 self.write("\n<dd>") 860 } 861 } 862 Tag::Subscript => self.write("<sub>"), 863 Tag::Superscript => self.write("<sup>"), 864 Tag::Emphasis => self.write("<em>"), 865 Tag::Strong => self.write("<strong>"), 866 Tag::Strikethrough => self.write("<del>"), 867 Tag::Link { 868 link_type: LinkType::Email, 869 dest_url, 870 title, 871 id: _, 872 } => { 873 self.write("<a href=\"mailto:")?; 874 escape_href(&mut self.writer, &dest_url)?; 875 if !title.is_empty() { 876 self.write("\" title=\"")?; 877 escape_html(&mut self.writer, &title)?; 878 } 879 self.write("\">") 880 } 881 Tag::Link { 882 link_type: _, 883 dest_url, 884 title, 885 id: _, 886 } => { 887 self.write("<a href=\"")?; 888 escape_href(&mut self.writer, &dest_url)?; 889 if !title.is_empty() { 890 self.write("\" title=\"")?; 891 escape_html(&mut self.writer, &title)?; 892 } 893 self.write("\">") 894 } 895 Tag::Image { 896 link_type, 897 dest_url, 898 title, 899 id, 900 attrs, 901 } => { 902 //println!("Image tag {}", dest_url); 903 self.write_image(Tag::Image { 904 link_type, 905 dest_url, 906 title, 907 id, 908 attrs, 909 }) 910 .await 911 } 912 Tag::Embed { 913 embed_type, 914 dest_url, 915 title, 916 id, 917 attrs, 918 } => { 919 //println!("Embed {:?}: {} - {}", embed_type, title, dest_url); 920 if let Some(attrs) = attrs { 921 if let Some((_, content)) = attrs 922 .attrs 923 .iter() 924 .find(|(attr, _)| attr.as_ref() == "content") 925 { 926 match embed_type { 927 EmbedType::Image => { 928 self.write_image(Tag::Image { 929 link_type: LinkType::Inline, 930 dest_url, 931 title, 932 id, 933 attrs: Some(attrs.clone()), 934 }) 935 .await? 936 } 937 EmbedType::Comments => { 938 self.write("leaflet would go here\n")?; 939 } 940 EmbedType::Post => { 941 // Bluesky post embed, basically just render the raw html we got 942 self.write(content)?; 943 self.write_newline()?; 944 } 945 EmbedType::Markdown => { 946 // let context = self 947 // .context 948 // .context 949 // .clone_with_path(&Path::new(&dest_url.to_string())); 950 // let callback = 951 // if let Some(dir_contents) = context.dir_contents.clone() { 952 // Some(VaultBrokenLinkCallback { 953 // vault_contents: dir_contents, 954 // }) 955 // } else { 956 // None 957 // }; 958 // let parser = Parser::new_with_broken_link_callback( 959 // &content, 960 // context.md_options, 961 // callback, 962 // ); 963 // let iterator = ContextIterator::default(parser); 964 // let mut stream = NotebookProcessor::new(context, iterator); 965 // while let Some(event) = stream.next().await { 966 // self.process_event(event).await?; 967 // } 968 // 969 self.write("markdown embed would go here\n")?; 970 } 971 EmbedType::Leaflet => { 972 self.write("leaflet would go here\n")?; 973 } 974 EmbedType::Other => { 975 self.write("other embed would go here\n")?; 976 } 977 } 978 } 979 } else { 980 self.write("<iframe src=\"")?; 981 escape_href(&mut self.writer, &dest_url)?; 982 self.write("\" title=\"")?; 983 escape_html(&mut self.writer, &title)?; 984 if !id.is_empty() { 985 self.write("\" id=\"")?; 986 escape_html(&mut self.writer, &id)?; 987 self.write("\"")?; 988 } 989 if let Some(attrs) = attrs { 990 self.write(" ")?; 991 if !attrs.classes.is_empty() { 992 self.write("class=\"")?; 993 for class in &attrs.classes { 994 escape_html(&mut self.writer, class)?; 995 self.write(" ")?; 996 } 997 self.write("\" ")?; 998 } 999 if !attrs.attrs.is_empty() { 1000 for (attr, value) in &attrs.attrs { 1001 escape_html(&mut self.writer, attr)?; 1002 self.write("=\"")?; 1003 escape_html(&mut self.writer, value)?; 1004 self.write("\" ")?; 1005 } 1006 } 1007 } 1008 self.write("/>")?; 1009 } 1010 Ok(()) 1011 } 1012 Tag::WeaverBlock(_, attrs) => { 1013 self.in_non_writing_block = true; 1014 self.weaver_block_buffer.clear(); 1015 // Store attrs from Start tag, will merge with parsed text on End 1016 if !attrs.classes.is_empty() || !attrs.attrs.is_empty() { 1017 self.pending_block_attrs = Some(attrs.into_static()); 1018 } 1019 Ok(()) 1020 } 1021 Tag::FootnoteDefinition(name) => { 1022 // Check if this matches a pending footnote reference (sidenote case) 1023 let is_sidenote = self 1024 .pending_footnote 1025 .as_ref() 1026 .map(|(n, _)| n.as_ref() == name.as_ref()) 1027 .unwrap_or(false); 1028 1029 if is_sidenote { 1030 // Emit sidenote structure at reference position 1031 let (_, number) = self.pending_footnote.take().unwrap(); 1032 let id = format!("sn-{}", number); 1033 1034 // Emit: <label><input/><span class="sidenote"> 1035 self.write("<label for=\"")?; 1036 self.write(&id)?; 1037 self.write("\" class=\"sidenote-number\"></label>")?; 1038 self.write("<input type=\"checkbox\" id=\"")?; 1039 self.write(&id)?; 1040 self.write("\" class=\"margin-toggle\"/>")?; 1041 self.write("<span class=\"sidenote\">")?; 1042 1043 // Write any buffered content AFTER the sidenote span closes 1044 // (we'll do this in end_tag) 1045 self.in_sidenote = true; 1046 } else { 1047 // Traditional footnote - close any deferred paragraph (which also flushes pending ref) 1048 self.close_deferred_paragraph()?; 1049 1050 if self.end_newline { 1051 self.write("<div class=\"footnote-definition\" id=\"")?; 1052 } else { 1053 self.write("\n<div class=\"footnote-definition\" id=\"")?; 1054 } 1055 escape_html(&mut self.writer, &name)?; 1056 self.write("\"><sup class=\"footnote-definition-label\">")?; 1057 let len = self.numbers.len() + 1; 1058 let number = *self.numbers.entry(name.into_static()).or_insert(len); 1059 write!(&mut self.writer, "{}", number)?; 1060 self.write("</sup>")?; 1061 } 1062 Ok(()) 1063 } 1064 Tag::MetadataBlock(_) => { 1065 self.in_non_writing_block = true; 1066 Ok(()) 1067 } 1068 } 1069 } 1070 1071 async fn write_image(&mut self, tag: Tag<'input>) -> Result<(), W::Error> { 1072 if let Tag::Image { 1073 link_type: _, 1074 dest_url, 1075 title, 1076 id: _, 1077 attrs, 1078 } = tag 1079 { 1080 self.write("<img src=\"")?; 1081 escape_href(&mut self.writer, &dest_url)?; 1082 if let Some(attrs) = attrs { 1083 if !attrs.classes.is_empty() { 1084 self.write("\" class=\"")?; 1085 for class in &attrs.classes { 1086 escape_html(&mut self.writer, class)?; 1087 self.write(" ")?; 1088 } 1089 self.write("\" ")?; 1090 } else { 1091 self.write("\" ")?; 1092 } 1093 if !attrs.attrs.is_empty() { 1094 for (attr, value) in &attrs.attrs { 1095 escape_html(&mut self.writer, attr)?; 1096 self.write("=\"")?; 1097 escape_html(&mut self.writer, value)?; 1098 self.write("\" ")?; 1099 } 1100 } 1101 } else { 1102 self.write("\" ")?; 1103 } 1104 self.write("alt=\"")?; 1105 self.raw_text().await?; 1106 if !title.is_empty() { 1107 self.write("\" title=\"")?; 1108 escape_html(&mut self.writer, &title)?; 1109 } 1110 self.write("\" />") 1111 } else { 1112 self.write_newline() 1113 } 1114 } 1115}