Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: try to do markdown components in a single pass (#19)

* feat: try to do markdown components in a single pass

* feat: do it fully in one pass

authored by

Erika and committed by
GitHub
0db640a3 30e43e26

+328 -336
+325 -333
crates/maudit/src/content/markdown.rs
··· 2 2 3 3 use glob::glob as glob_fs; 4 4 use log::warn; 5 - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; 5 + use pulldown_cmark::{html::push_html, CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; 6 6 use serde::de::DeserializeOwned; 7 7 8 8 pub mod components; ··· 223 223 } 224 224 225 225 // TODO: Prettier errors for serialization errors (e.g. missing fields) 226 + // TODO: Support TOML frontmatters 226 227 let mut parsed = serde_yml::from_str::<T>(&frontmatter).unwrap(); 227 228 228 229 let headings_internal = find_headings(&content_events); ··· 263 264 entries 264 265 } 265 266 266 - fn get_text_from_events(parser_slice: &[Event]) -> String { 267 - let mut title = String::new(); 268 - 269 - for event in parser_slice.iter() { 267 + fn get_text_from_events(events_slice: &[Event]) -> String { 268 + events_slice.iter().fold(String::new(), |mut acc, event| { 270 269 match event { 271 - Event::Text(text) | Event::Code(text) => title += text, 272 - _ => continue, 270 + Event::Text(text) | Event::Code(text) => acc.push_str(text), 271 + _ => {} 273 272 } 274 - } 275 - 276 - title 273 + acc 274 + }) 277 275 } 278 276 279 277 fn find_headings(events: &[Event]) -> Vec<InternalHeadingEvent> { 280 - let mut heading_refs = vec![]; 278 + let mut headings = Vec::new(); 281 279 282 280 for (i, event) in events.iter().enumerate() { 283 281 match event { 284 282 Event::Start(Tag::Heading { 285 283 level, id, classes, .. 286 284 }) => { 287 - heading_refs.push(InternalHeadingEvent::new( 285 + headings.push(InternalHeadingEvent::new( 288 286 i, 289 287 *level as u32, 290 - id.clone().map(String::from), 291 - &classes 292 - .iter() 293 - .map(|c| c.to_string()) 294 - .collect::<Vec<String>>(), 288 + id.as_ref().map(|s| s.to_string()), 289 + &classes.iter().map(|c| c.to_string()).collect::<Vec<_>>(), 295 290 )); 296 291 } 297 292 Event::End(TagEnd::Heading { .. }) => { 298 - heading_refs 299 - .last_mut() 300 - .expect("Heading end before start?") 301 - .end = i; 293 + if let Some(heading) = headings.last_mut() { 294 + heading.end = i; 295 + } 302 296 } 303 - _ => (), 297 + _ => {} 304 298 } 305 299 } 306 300 307 - heading_refs 301 + headings 308 302 } 309 303 310 304 /// Render Markdown content to HTML with optional custom components. ··· 355 349 let mut code_block = None; 356 350 let mut code_block_content = String::new(); 357 351 let mut in_frontmatter = false; 358 - let mut events = Vec::new(); 352 + let mut in_image = false; 353 + let mut events = Parser::new_ext(content, parser_options).collect::<Vec<Event>>(); 354 + 355 + let options_with_components = options 356 + .as_ref() 357 + .filter(|o| o.components.has_any_components()); 359 358 360 - // Do a first pass to collect body events 361 - for event in Parser::new_ext(content, parser_options) { 362 - match event { 359 + for i in 0..events.len() { 360 + match &events[i] { 363 361 Event::Start(Tag::MetadataBlock(_)) => { 364 362 in_frontmatter = true; 363 + continue; 365 364 } 366 365 Event::End(TagEnd::MetadataBlock(_)) => { 367 366 in_frontmatter = false; 368 - } 369 - Event::Text(ref text) => { 370 - if !in_frontmatter { 371 - if code_block.is_some() { 372 - code_block_content.push_str(text); 373 - } else { 374 - events.push(event); 375 - } 376 - } 367 + continue; 377 368 } 378 369 379 370 // TODO: Handle this differently so it's compatible with the component system - erika, 2025-08-24 380 - Event::Start(Tag::CodeBlock(ref kind)) => { 381 - if let CodeBlockKind::Fenced(ref fence) = kind { 382 - let (block, begin) = CodeBlock::new(fence); 383 - code_block = Some(block); 384 - events.push(Event::Html(begin.into())); 385 - } 371 + Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref fence))) => { 372 + let (block, begin) = CodeBlock::new(fence); 373 + code_block = Some(block); 374 + events[i] = Event::Html(begin.into()); 386 375 } 376 + 387 377 Event::End(TagEnd::CodeBlock) => { 388 378 if let Some(ref mut code_block) = code_block { 389 379 let html = code_block.highlight(&code_block_content); 390 - events.push(Event::Html(html.unwrap().into())); 380 + events[i] = 381 + Event::Html(format!("{}{}", html.unwrap(), "</code></pre>\n").into()); 391 382 } 392 383 code_block = None; 393 384 code_block_content.clear(); 394 - events.push(Event::Html("</code></pre>\n".into())); 395 385 } 396 386 397 - _ => { 398 - events.push(event); 399 - } 400 - } 401 - } 387 + // TODO: User should be able to replace the text component too perhaps, but it'd require merging the text events 388 + Event::Text(ref text) => { 389 + if !in_frontmatter { 390 + if in_image { 391 + // This seem to work to create "an empty event", but it's not ideal. Using `events.remove` is probably 392 + // more idiomatic, but it's also less efficient. Wonky situation. 393 + events[i] = Event::Html("".into()); 394 + } else if code_block.is_some() { 395 + code_block_content.push_str(text); 402 396 403 - // If we don't have a custom heading component, use default heading rendering 404 - if options.is_none_or(|o| o.components.heading.is_none()) { 405 - let headings = find_headings(&events); 406 - 407 - for heading in &headings { 408 - let heading_content = get_text_from_events(&events[heading.start..heading.end]); 409 - let slug: String = slugger.slugify(&heading_content); 410 - 411 - events[heading.start] = Event::Html( 412 - format!( 413 - "<h{} id=\"{}\" class=\"{}\">", 414 - heading.level, 415 - heading.id.clone().unwrap_or(slug), 416 - heading.classes.join(" ") 417 - ) 418 - .into(), 419 - ); 420 - } 421 - } 422 - 423 - // Second pass: transform events with custom components only if needed 424 - if let Some(options) = options { 425 - if options.components.has_any_components() { 426 - transform_events_with_components(&mut events, &options.components, &mut slugger); 427 - } 428 - } 429 - 430 - pulldown_cmark::html::push_html(&mut html_output, events.into_iter()); 431 - html_output 432 - } 433 - 434 - fn transform_events_with_components( 435 - events: &mut Vec<Event>, 436 - components: &MarkdownComponents, 437 - slugger: &mut slugger::Slugger, 438 - ) { 439 - let mut i = 0; 397 + events[i] = Event::Html("".into()); 398 + } 399 + } else { 400 + events[i] = Event::Html("".into()); 401 + } 402 + } 440 403 441 - while i < events.len() { 442 - match &events[i] { 443 404 // Headings 444 405 Event::Start(Tag::Heading { 445 406 level, id, classes, .. 446 407 }) => { 447 - if let Some(component) = &components.heading { 448 - let heading_content = 449 - if let Some(end_index) = find_matching_heading_end(events, i) { 450 - get_text_from_events(&events[i + 1..end_index]) 451 - } else { 452 - String::new() 453 - }; 454 - let slug = slugger.slugify(&heading_content); 455 - let heading_id = id.as_ref().map(|s| s.as_ref()).unwrap_or(&slug); 456 - let classes_vec: Vec<&str> = classes.iter().map(|c| c.as_ref()).collect(); 408 + // Extract heading content for slug generation only 409 + let heading_content = if let Some(end_index) = find_matching_heading_end(&events, i) 410 + { 411 + get_text_from_events(&events[i + 1..end_index]) 412 + } else { 413 + String::new() 414 + }; 415 + 416 + let slug = slugger.slugify(&heading_content); 417 + let heading_id = id.as_ref().map(|s| s.as_ref()).unwrap_or(&slug); 457 418 419 + if let Some(component) = options.and_then(|opts| opts.components.heading.as_ref()) { 420 + let classes_vec: Vec<&str> = classes.iter().map(|c| c.as_ref()).collect(); 458 421 let custom_html = 459 422 component.render_start(*level as u8, Some(heading_id), &classes_vec); 460 423 events[i] = Event::Html(custom_html.into()); 424 + } else { 425 + events[i] = Event::Html( 426 + format!( 427 + "<{} id=\"{}\" class=\"{}\">", 428 + level, 429 + heading_id, 430 + classes.join(" ") 431 + ) 432 + .into(), 433 + ); 461 434 } 462 435 } 463 436 Event::End(TagEnd::Heading(level)) => { 464 - if let Some(component) = &components.heading { 437 + if let Some(component) = options.and_then(|opts| opts.components.heading.as_ref()) { 465 438 let custom_html = component.render_end(*level as u8); 466 439 events[i] = Event::Html(custom_html.into()); 440 + } else { 441 + events[i] = Event::Html(format!("</h{}>", *level as u32).into()); 467 442 } 468 443 } 469 444 470 - // Paragraphs 471 - Event::Start(Tag::Paragraph) => { 472 - if let Some(component) = &components.paragraph { 473 - let custom_html = component.render_start(); 474 - events[i] = Event::Html(custom_html.into()); 445 + // All other events pass through unchanged 446 + _ => {} 447 + } 448 + 449 + // Handle using components for all the different events 450 + if let Some(options) = options_with_components { 451 + match &events[i] { 452 + // Paragraphs 453 + Event::Start(Tag::Paragraph) => { 454 + if let Some(component) = &options.components.paragraph { 455 + let custom_html = component.render_start(); 456 + events[i] = Event::Html(custom_html.into()); 457 + } 475 458 } 476 - } 477 - Event::End(TagEnd::Paragraph) => { 478 - if let Some(component) = &components.paragraph { 479 - let custom_html = component.render_end(); 480 - events[i] = Event::Html(custom_html.into()); 459 + Event::End(TagEnd::Paragraph) => { 460 + if let Some(component) = &options.components.paragraph { 461 + let custom_html = component.render_end(); 462 + events[i] = Event::Html(custom_html.into()); 463 + } 481 464 } 482 - } 483 465 484 - // Links 485 - Event::Start(Tag::Link { 486 - link_type, 487 - dest_url, 488 - title, 489 - .. 490 - }) => { 491 - if let Some(component) = &components.link { 492 - let link_type_converted: LinkType = link_type.into(); 493 - let title_str = if title.is_empty() { 494 - None 495 - } else { 496 - Some(title.as_ref()) 497 - }; 498 - let custom_html = 499 - component.render_start(dest_url.as_ref(), title_str, link_type_converted); 500 - events[i] = Event::Html(custom_html.into()); 466 + // Links, i.e [link text](url) 467 + // TODO: Verify that everything works when using different types of link 468 + Event::Start(Tag::Link { 469 + link_type, 470 + dest_url, 471 + title, 472 + .. 473 + }) => { 474 + if let Some(component) = &options.components.link { 475 + let link_type_converted: LinkType = link_type.into(); 476 + let title_str = if title.is_empty() { 477 + None 478 + } else { 479 + Some(title.as_ref()) 480 + }; 481 + let custom_html = component.render_start( 482 + dest_url.as_ref(), 483 + title_str, 484 + link_type_converted, 485 + ); 486 + events[i] = Event::Html(custom_html.into()); 487 + } 501 488 } 502 - } 503 - Event::End(TagEnd::Link) => { 504 - if let Some(component) = &components.link { 505 - let custom_html = component.render_end(); 506 - events[i] = Event::Html(custom_html.into()); 489 + Event::End(TagEnd::Link) => { 490 + if let Some(component) = &options.components.link { 491 + let custom_html = component.render_end(); 492 + events[i] = Event::Html(custom_html.into()); 493 + } 507 494 } 508 - } 509 495 510 - // Images - special case that consumes multiple events 511 - Event::Start(Tag::Image { 512 - dest_url, title, .. 513 - }) => { 514 - if let Some(component) = &components.image { 515 - // For images, we need to get the alt text from content between start and end 516 - let alt_text = if let Some(end_index) = find_matching_image_end(events, i) { 517 - get_text_from_events(&events[i + 1..end_index]) 518 - } else { 519 - String::new() 520 - }; 521 - let title_str = if title.is_empty() { 522 - None 523 - } else { 524 - Some(title.as_ref()) 525 - }; 526 - let custom_html = component.render(dest_url.as_ref(), &alt_text, title_str); 527 - events[i] = Event::Html(custom_html.into()); 528 - 529 - // Skip to the end tag and remove intermediate events 530 - if let Some(end_index) = find_matching_image_end(events, i) { 531 - // Remove all events between start and end (inclusive of end) 532 - events.drain(i + 1..=end_index); 496 + // Images, i.e ![alt text](url) 497 + // TODO: Verify that everything works when using different types of images 498 + Event::Start(Tag::Image { 499 + dest_url, title, .. 500 + }) => { 501 + in_image = true; 502 + if let Some(component) = &options.components.image { 503 + // For images, we need to get the alt text from content between start and end 504 + let alt_text = if let Some(end_index) = find_matching_image_end(&events, i) 505 + { 506 + get_text_from_events(&events[i + 1..end_index]) 507 + } else { 508 + String::new() 509 + }; 510 + let title_str = if title.is_empty() { 511 + None 512 + } else { 513 + Some(title.as_ref()) 514 + }; 515 + let custom_html = component.render(dest_url.as_ref(), &alt_text, title_str); 516 + events[i] = Event::Html(custom_html.into()); 533 517 } 534 518 } 535 - } 519 + 520 + Event::End(TagEnd::Image) => { 521 + // Images are a bit weird, the alt text is part of the text between the start and end tags 522 + // despite in some syntax being actually part of the image tag itself. Perhaps something I'm just not 523 + // familiar with. 524 + in_image = false; 525 + } 536 526 537 - // Bold (strong) 538 - Event::Start(Tag::Strong) => { 539 - if let Some(component) = &components.strong { 540 - let custom_html = component.render_start(); 541 - events[i] = Event::Html(custom_html.into()); 527 + // Bold (strong) 528 + Event::Start(Tag::Strong) => { 529 + if let Some(component) = &options.components.strong { 530 + let custom_html = component.render_start(); 531 + events[i] = Event::Html(custom_html.into()); 532 + } 542 533 } 543 - } 544 - Event::End(TagEnd::Strong) => { 545 - if let Some(component) = &components.strong { 546 - let custom_html = component.render_end(); 547 - events[i] = Event::Html(custom_html.into()); 534 + Event::End(TagEnd::Strong) => { 535 + if let Some(component) = &options.components.strong { 536 + let custom_html = component.render_end(); 537 + events[i] = Event::Html(custom_html.into()); 538 + } 548 539 } 549 - } 550 540 551 - // Italic (emphasis) 552 - Event::Start(Tag::Emphasis) => { 553 - if let Some(component) = &components.emphasis { 554 - let custom_html = component.render_start(); 555 - events[i] = Event::Html(custom_html.into()); 541 + // Italic (emphasis) 542 + Event::Start(Tag::Emphasis) => { 543 + if let Some(component) = &options.components.emphasis { 544 + let custom_html = component.render_start(); 545 + events[i] = Event::Html(custom_html.into()); 546 + } 556 547 } 557 - } 558 - Event::End(TagEnd::Emphasis) => { 559 - if let Some(component) = &components.emphasis { 560 - let custom_html = component.render_end(); 561 - events[i] = Event::Html(custom_html.into()); 548 + Event::End(TagEnd::Emphasis) => { 549 + if let Some(component) = &options.components.emphasis { 550 + let custom_html = component.render_end(); 551 + events[i] = Event::Html(custom_html.into()); 552 + } 562 553 } 563 - } 564 554 565 - // Inline Code 566 - Event::Code(code) => { 567 - if let Some(component) = &components.code { 568 - let custom_html = component.render(code.as_ref()); 569 - events[i] = Event::Html(custom_html.into()); 555 + // Inline Code 556 + Event::Code(code) => { 557 + if let Some(component) = &options.components.code { 558 + let custom_html = component.render(code.as_ref()); 559 + events[i] = Event::Html(custom_html.into()); 560 + } 570 561 } 571 - } 572 562 573 - // Blockquotes 574 - Event::Start(Tag::BlockQuote(kind)) => { 575 - if let Some(component) = &components.blockquote { 576 - let kind_converted = kind.as_ref().map(|k| k.into()); 577 - let custom_html = component.render_start(kind_converted); 578 - events[i] = Event::Html(custom_html.into()); 563 + // Blockquotes 564 + Event::Start(Tag::BlockQuote(kind)) => { 565 + if let Some(component) = &options.components.blockquote { 566 + let kind_converted = kind.as_ref().map(|k| k.into()); 567 + let custom_html = component.render_start(kind_converted); 568 + events[i] = Event::Html(custom_html.into()); 569 + } 579 570 } 580 - } 581 - Event::End(TagEnd::BlockQuote(kind)) => { 582 - if let Some(component) = &components.blockquote { 583 - let kind_converted = kind.as_ref().map(|k| k.into()); 584 - let custom_html = component.render_end(kind_converted); 585 - events[i] = Event::Html(custom_html.into()); 571 + Event::End(TagEnd::BlockQuote(kind)) => { 572 + if let Some(component) = &options.components.blockquote { 573 + let kind_converted = kind.as_ref().map(|k| k.into()); 574 + let custom_html = component.render_end(kind_converted); 575 + events[i] = Event::Html(custom_html.into()); 576 + } 586 577 } 587 - } 588 578 589 - // Hard Breaks 590 - Event::HardBreak => { 591 - if let Some(component) = &components.hard_break { 592 - let custom_html = component.render(); 593 - events[i] = Event::Html(custom_html.into()); 579 + // Hard Breaks 580 + Event::HardBreak => { 581 + if let Some(component) = &options.components.hard_break { 582 + let custom_html = component.render(); 583 + events[i] = Event::Html(custom_html.into()); 584 + } 594 585 } 595 - } 596 586 597 - // Horizontal Rules 598 - Event::Rule => { 599 - if let Some(component) = &components.horizontal_rule { 600 - let custom_html = component.render(); 601 - events[i] = Event::Html(custom_html.into()); 587 + // Horizontal Rules 588 + Event::Rule => { 589 + if let Some(component) = &options.components.horizontal_rule { 590 + let custom_html = component.render(); 591 + events[i] = Event::Html(custom_html.into()); 592 + } 602 593 } 603 - } 604 594 605 - // Lists 606 - Event::Start(Tag::List(first_number)) => { 607 - if let Some(component) = &components.list { 608 - let list_type = if first_number.is_some() { 609 - ListType::Ordered 610 - } else { 611 - ListType::Unordered 612 - }; 613 - let custom_html = component.render_start(list_type, *first_number); 614 - events[i] = Event::Html(custom_html.into()); 595 + // Lists 596 + Event::Start(Tag::List(first_number)) => { 597 + if let Some(component) = &options.components.list { 598 + let list_type = if first_number.is_some() { 599 + ListType::Ordered 600 + } else { 601 + ListType::Unordered 602 + }; 603 + let custom_html = component.render_start(list_type, *first_number); 604 + events[i] = Event::Html(custom_html.into()); 605 + } 615 606 } 616 - } 617 - Event::End(TagEnd::List(ordered)) => { 618 - if let Some(component) = &components.list { 619 - let list_type = if *ordered { 620 - ListType::Ordered 621 - } else { 622 - ListType::Unordered 623 - }; 624 - let custom_html = component.render_end(list_type); 625 - events[i] = Event::Html(custom_html.into()); 607 + Event::End(TagEnd::List(ordered)) => { 608 + if let Some(component) = &options.components.list { 609 + let list_type = if *ordered { 610 + ListType::Ordered 611 + } else { 612 + ListType::Unordered 613 + }; 614 + let custom_html = component.render_end(list_type); 615 + events[i] = Event::Html(custom_html.into()); 616 + } 626 617 } 627 - } 628 618 629 - // List Items, i.e. individual - item 630 - Event::Start(Tag::Item) => { 631 - if let Some(component) = &components.list_item { 632 - let custom_html = component.render_start(); 633 - events[i] = Event::Html(custom_html.into()); 619 + // List Items, i.e. individual - item 620 + Event::Start(Tag::Item) => { 621 + if let Some(component) = &options.components.list_item { 622 + let custom_html = component.render_start(); 623 + events[i] = Event::Html(custom_html.into()); 624 + } 634 625 } 635 - } 636 - Event::End(TagEnd::Item) => { 637 - if let Some(component) = &components.list_item { 638 - let custom_html = component.render_end(); 639 - events[i] = Event::Html(custom_html.into()); 626 + Event::End(TagEnd::Item) => { 627 + if let Some(component) = &options.components.list_item { 628 + let custom_html = component.render_end(); 629 + events[i] = Event::Html(custom_html.into()); 630 + } 640 631 } 641 - } 642 632 643 - // (GFM) Strikethrough, i.e. ~~strikethrough~~ 644 - Event::Start(Tag::Strikethrough) => { 645 - if let Some(component) = &components.strikethrough { 646 - let custom_html = component.render_start(); 647 - events[i] = Event::Html(custom_html.into()); 633 + // (GFM) Strikethrough, i.e. ~~strikethrough~~ 634 + Event::Start(Tag::Strikethrough) => { 635 + if let Some(component) = &options.components.strikethrough { 636 + let custom_html = component.render_start(); 637 + events[i] = Event::Html(custom_html.into()); 638 + } 648 639 } 649 - } 650 - Event::End(TagEnd::Strikethrough) => { 651 - if let Some(component) = &components.strikethrough { 652 - let custom_html = component.render_end(); 653 - events[i] = Event::Html(custom_html.into()); 640 + Event::End(TagEnd::Strikethrough) => { 641 + if let Some(component) = &options.components.strikethrough { 642 + let custom_html = component.render_end(); 643 + events[i] = Event::Html(custom_html.into()); 644 + } 654 645 } 655 - } 656 646 657 - // (GFM) Task List Markers, i.e. - [ ] item 658 - Event::TaskListMarker(checked) => { 659 - if let Some(component) = &components.task_list_marker { 660 - let custom_html = component.render(*checked); 661 - events[i] = Event::Html(custom_html.into()); 647 + // (GFM) Task List Markers, i.e. - [ ] item 648 + Event::TaskListMarker(checked) => { 649 + if let Some(component) = &options.components.task_list_marker { 650 + let custom_html = component.render(*checked); 651 + events[i] = Event::Html(custom_html.into()); 652 + } 662 653 } 663 - } 664 654 665 - // (GFM) Tables, i.e. | Header | Header | 666 - // |--------|--------| 667 - // | Cell | Cell | 668 - // |--------|--------| 669 - Event::Start(Tag::Table(alignments)) => { 670 - if let Some(component) = &components.table { 671 - let alignment_vec: Vec<TableAlignment> = alignments 672 - .iter() 673 - .map(|a| match a { 674 - pulldown_cmark::Alignment::Left => TableAlignment::Left, 675 - pulldown_cmark::Alignment::Center => TableAlignment::Center, 676 - pulldown_cmark::Alignment::Right => TableAlignment::Right, 677 - pulldown_cmark::Alignment::None => TableAlignment::Left, 678 - }) 679 - .collect(); 680 - let custom_html = component.render_start(&alignment_vec); 681 - events[i] = Event::Html(custom_html.into()); 655 + // (GFM) Tables, i.e. | Header | Header | 656 + // |--------|--------| 657 + // | Cell | Cell | 658 + // |--------|--------| 659 + Event::Start(Tag::Table(alignments)) => { 660 + if let Some(component) = &options.components.table { 661 + let alignment_vec: Vec<TableAlignment> = alignments 662 + .iter() 663 + .map(|a| match a { 664 + pulldown_cmark::Alignment::Left => TableAlignment::Left, 665 + pulldown_cmark::Alignment::Center => TableAlignment::Center, 666 + pulldown_cmark::Alignment::Right => TableAlignment::Right, 667 + pulldown_cmark::Alignment::None => TableAlignment::Left, 668 + }) 669 + .collect(); 670 + let custom_html = component.render_start(&alignment_vec); 671 + events[i] = Event::Html(custom_html.into()); 672 + } 682 673 } 683 - } 684 - Event::End(TagEnd::Table) => { 685 - if let Some(component) = &components.table { 686 - let custom_html = component.render_end(); 687 - events[i] = Event::Html(custom_html.into()); 674 + Event::End(TagEnd::Table) => { 675 + if let Some(component) = &options.components.table { 676 + let custom_html = component.render_end(); 677 + events[i] = Event::Html(custom_html.into()); 678 + } 688 679 } 689 - } 690 680 691 - // (GFM) Table Heads, i.e. | Header | Header | 692 - Event::Start(Tag::TableHead) => { 693 - if let Some(component) = &components.table_head { 694 - let custom_html = component.render_start(); 695 - events[i] = Event::Html(custom_html.into()); 681 + // (GFM) Table Heads, i.e. | Header | Header | 682 + Event::Start(Tag::TableHead) => { 683 + if let Some(component) = &options.components.table_head { 684 + let custom_html = component.render_start(); 685 + events[i] = Event::Html(custom_html.into()); 686 + } 696 687 } 697 - } 698 - Event::End(TagEnd::TableHead) => { 699 - if let Some(component) = &components.table_head { 700 - let custom_html = component.render_end(); 701 - events[i] = Event::Html(custom_html.into()); 688 + Event::End(TagEnd::TableHead) => { 689 + if let Some(component) = &options.components.table_head { 690 + let custom_html = component.render_end(); 691 + events[i] = Event::Html(custom_html.into()); 692 + } 702 693 } 703 - } 704 694 705 - // (GFM) Table Rows, i.e. | Cell | Cell | 706 - Event::Start(Tag::TableRow) => { 707 - if let Some(component) = &components.table_row { 708 - let custom_html = component.render_start(); 709 - events[i] = Event::Html(custom_html.into()); 695 + // (GFM) Table Rows, i.e. | Cell | Cell | 696 + Event::Start(Tag::TableRow) => { 697 + if let Some(component) = &options.components.table_row { 698 + let custom_html = component.render_start(); 699 + events[i] = Event::Html(custom_html.into()); 700 + } 710 701 } 711 - } 712 - Event::End(TagEnd::TableRow) => { 713 - if let Some(component) = &components.table_row { 714 - let custom_html = component.render_end(); 715 - events[i] = Event::Html(custom_html.into()); 702 + Event::End(TagEnd::TableRow) => { 703 + if let Some(component) = &options.components.table_row { 704 + let custom_html = component.render_end(); 705 + events[i] = Event::Html(custom_html.into()); 706 + } 716 707 } 717 - } 718 708 719 - // (GFM) Table Cells, i.e. individual | Cell | 720 - Event::Start(Tag::TableCell) => { 721 - if let Some(component) = &components.table_cell { 722 - // For now, assume it's not a header and no specific alignment 723 - // TODO: Track context to determine if we're in a table head and column alignment 724 - let custom_html = component.render_start(false, None); 725 - events[i] = Event::Html(custom_html.into()); 709 + // (GFM) Table Cells, i.e. individual | Cell | 710 + Event::Start(Tag::TableCell) => { 711 + if let Some(component) = &options.components.table_cell { 712 + // For now, assume it's not a header and no specific alignment 713 + // TODO: Track context to determine if we're in a table head and column alignment 714 + let custom_html = component.render_start(false, None); 715 + events[i] = Event::Html(custom_html.into()); 716 + } 726 717 } 727 - } 728 - Event::End(TagEnd::TableCell) => { 729 - if let Some(component) = &components.table_cell { 730 - // TODO: Track context to determine if we're in a table head 731 - let custom_html = component.render_end(false); 732 - events[i] = Event::Html(custom_html.into()); 718 + Event::End(TagEnd::TableCell) => { 719 + if let Some(component) = &options.components.table_cell { 720 + // TODO: Track context to determine if we're in a table head 721 + let custom_html = component.render_end(false); 722 + events[i] = Event::Html(custom_html.into()); 723 + } 733 724 } 725 + _ => {} 734 726 } 735 - 736 - // All other events pass through unchanged 737 - _ => {} 738 727 } 739 - i += 1; 740 728 } 729 + 730 + events.retain(|e| match e { 731 + Event::Text(content) | Event::Html(content) => !content.is_empty(), 732 + _ => true, 733 + }); 734 + 735 + push_html(&mut html_output, events.into_iter()); 736 + html_output 741 737 } 742 738 743 739 fn find_matching_heading_end(events: &[Event], start_index: usize) -> Option<usize> { 744 - for (i, event) in events.iter().enumerate().skip(start_index + 1) { 745 - if matches!(event, Event::End(TagEnd::Heading(_))) { 746 - return Some(i); 747 - } 748 - } 749 - None 740 + events[start_index + 1..] 741 + .iter() 742 + .position(|event| matches!(event, Event::End(TagEnd::Heading(_)))) 743 + .map(|offset| start_index + 1 + offset) 750 744 } 751 745 752 746 fn find_matching_image_end(events: &[Event], start_index: usize) -> Option<usize> { 753 - for (i, event) in events.iter().enumerate().skip(start_index + 1) { 754 - if matches!(event, Event::End(TagEnd::Image)) { 755 - return Some(i); 756 - } 757 - } 758 - None 747 + events[start_index + 1..] 748 + .iter() 749 + .position(|event| matches!(event, Event::End(TagEnd::Image))) 750 + .map(|offset| start_index + 1 + offset) 759 751 } 760 752 761 753 #[cfg(test)]
+1 -1
examples/markdown-components/src/components.rs
··· 75 75 76 76 format!( 77 77 "<figure class=\"image-wrapper\"><img src=\"{}\" alt=\"{}\" class=\"responsive-image\"{} /><figcaption>{}</figcaption></figure>", 78 - url, alt, title_attr, alt 78 + url, alt, title_attr, title.unwrap_or_default() 79 79 ) 80 80 } 81 81 }
+2 -2
website/content/news/maudit01.md
··· 48 48 49 49 In general, if there's a compromise to be made, we prefer to optimize for developer experience over raw performance. However, we still aim to keep Maudit fast enough for most use cases. 50 50 51 - On a 2020 M1 MacBook Pro, [we've found that the final binary of a Maudit project can build a project with 4000 Markdown files in a little over 900ms](https://github.com/bruits/maudit/tree/main/crates/md-benchmark), which we consider quite reasonable. 51 + On a 2020 M1 MacBook Pro, [we've found that the final binary of a Maudit project can build a project with 4000 Markdown files in a little over 900ms](https://github.com/bruits/maudit/tree/main/benchmarks/md-benchmark), which we consider quite reasonable. 52 52 53 - [![Build Status](/01-performance.png)](https://github.com/bruits/maudit/tree/main/crates/md-benchmark) 53 + [![A graph showing the performance of Maudit building 250, 500, 1000, 2000 and 4000 pages. Respectively, it takes 55ms, 113ms, 253ms, 504ms and 922ms for the build to complete.](/01-performance.png)](https://github.com/bruits/maudit/tree/main/benchmarks/md-benchmark) 54 54 55 55 As we add more features, it's possible that Maudit will become slower, but we'll monitor performance and ensure that, yeah, it's reasonably fast. 56 56