Rust library to generate static websites
5
fork

Configure Feed

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

refactor: move frontmatter parsing to a re-usable function

Princesseuh 7fab1ab6 f29e8e0e

+83 -50
+83 -50
crates/maudit/src/content/markdown.rs
··· 197 197 198 198 // Clone content for the closure 199 199 let content_clone = content.clone(); 200 - let data_loader = Box::new(move |_: &mut RouteContext| { 201 - let mut slugger = slugger::Slugger::new(); 202 - 203 - let mut options = Options::empty(); 204 - options.insert( 205 - Options::ENABLE_YAML_STYLE_METADATA_BLOCKS | Options::ENABLE_HEADING_ATTRIBUTES, 206 - ); 207 - 208 - let mut frontmatter = String::new(); 209 - let mut in_frontmatter = false; 210 - 211 - let mut content_events = Vec::new(); 212 - for (event, _) in Parser::new_ext(&content_clone, options).into_offset_iter() { 213 - match event { 214 - Event::Start(Tag::MetadataBlock(_)) => in_frontmatter = true, 215 - Event::End(TagEnd::MetadataBlock(_)) => in_frontmatter = false, 216 - Event::Text(ref text) => { 217 - if in_frontmatter { 218 - frontmatter.push_str(text); 219 - } else { 220 - content_events.push(event); 221 - } 222 - } 223 - _ => content_events.push(event), 224 - } 225 - } 226 - 227 - // TODO: Prettier errors for serialization errors (e.g. missing fields) 228 - // TODO: Support TOML frontmatters 229 - let mut parsed = serde_yml::from_str::<T>(&frontmatter).unwrap(); 230 - 231 - let headings_internal = find_headings(&content_events); 232 - 233 - let mut headings = vec![]; 234 - for heading in headings_internal { 235 - let heading_content = 236 - get_text_from_events(&content_events[heading.start..heading.end]); 237 - let slug: String = slugger.slugify(&heading_content); 238 - 239 - headings.push(MarkdownHeading { 240 - title: heading_content, 241 - id: heading.id.unwrap_or(slug), 242 - level: heading.level as u8, 243 - classes: heading.classes, 244 - }); 245 - } 246 - 247 - parsed.set_headings(headings); 248 - parsed 249 - }); 200 + let data_loader = 201 + Box::new(move |_: &mut RouteContext| parse_markdown_with_frontmatter(&content_clone)); 250 202 251 203 // Perhaps not ideal, but I don't know better. We're at the "get it working" stage - erika, 2025-08-24 252 204 // Ideally, we'd at least avoid the allocation here whenever `options` is None, not sure how to do that ergonomically ··· 775 727 776 728 push_html(&mut html_output, events.into_iter()); 777 729 html_output 730 + } 731 + 732 + /// Parse Markdown content with frontmatter and extract headings. 733 + /// 734 + /// This function extracts YAML frontmatter from markdown content, deserializes it into the specified type, 735 + /// and automatically populates the headings for table of contents generation. 736 + /// 737 + /// ## Example 738 + /// ```rs 739 + /// use maudit::content::{parse_markdown_with_frontmatter, markdown_entry}; 740 + /// 741 + /// #[markdown_entry] 742 + /// pub struct ArticleContent { 743 + /// pub title: String, 744 + /// pub description: String, 745 + /// } 746 + /// 747 + /// let markdown = r#"--- 748 + /// title: "My Article" 749 + /// description: "A great article" 750 + /// --- 751 + /// 752 + /// # Introduction 753 + /// 754 + /// This is the content. 755 + /// "#; 756 + /// 757 + /// let parsed: ArticleContent = parse_markdown_with_frontmatter(markdown); 758 + /// assert_eq!(parsed.title, "My Article"); 759 + /// assert_eq!(parsed.get_headings().len(), 1); 760 + /// ``` 761 + pub fn parse_markdown_with_frontmatter<T>(content: &str) -> T 762 + where 763 + T: DeserializeOwned + MarkdownContent + InternalMarkdownContent, 764 + { 765 + let mut slugger = slugger::Slugger::new(); 766 + 767 + let mut options = Options::empty(); 768 + options.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS | Options::ENABLE_HEADING_ATTRIBUTES); 769 + 770 + let mut frontmatter = String::new(); 771 + let mut in_frontmatter = false; 772 + 773 + let mut content_events = Vec::new(); 774 + for (event, _) in Parser::new_ext(content, options).into_offset_iter() { 775 + match event { 776 + Event::Start(Tag::MetadataBlock(_)) => in_frontmatter = true, 777 + Event::End(TagEnd::MetadataBlock(_)) => in_frontmatter = false, 778 + Event::Text(ref text) => { 779 + if in_frontmatter { 780 + frontmatter.push_str(text); 781 + } else { 782 + content_events.push(event); 783 + } 784 + } 785 + _ => content_events.push(event), 786 + } 787 + } 788 + 789 + // TODO: Prettier errors for serialization errors (e.g. missing fields) 790 + // TODO: Support TOML frontmatters 791 + let mut parsed = serde_yml::from_str::<T>(&frontmatter) 792 + .unwrap_or_else(|e| panic!("Failed to parse YAML frontmatter: {}, {}", e, frontmatter)); 793 + 794 + let headings_internal = find_headings(&content_events); 795 + 796 + let mut headings = vec![]; 797 + for heading in headings_internal { 798 + let heading_content = get_text_from_events(&content_events[heading.start..heading.end]); 799 + let slug: String = slugger.slugify(&heading_content); 800 + 801 + headings.push(MarkdownHeading { 802 + title: heading_content, 803 + id: heading.id.unwrap_or(slug), 804 + level: heading.level as u8, 805 + classes: heading.classes, 806 + }); 807 + } 808 + 809 + parsed.set_headings(headings); 810 + parsed 778 811 } 779 812 780 813 fn find_matching_heading_end(events: &[Event], start_index: usize) -> Option<usize> {