From 3752cc5234d0e9f692c6f40b3083e9ada8ae97e7 Mon Sep 17 00:00:00 2001 From: Timothy DeHerrera Date: Sat, 31 Jan 2026 22:50:05 -0700 Subject: [PATCH] fix(render): properly capture image alt text from markdown pulldown-cmark emits alt text as Text events between Image start/end tags. Previously, images were rendered immediately on the Start event with empty alt text, losing the user-provided description. Refactored to accumulate alt text using the same pattern as code block handling: state variables track image attributes and alt content, then the full tag is rendered on the End event. Also omits the title attribute entirely when no title is provided, producing cleaner HTML output. Fixes Lighthouse "Image elements have [alt] attributes" audit issue. --- src/render.rs | 73 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/render.rs b/src/render.rs index 66a70f0..8d25c6c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -16,6 +16,10 @@ pub fn markdown_to_html(markdown: &str) -> String { let mut code_block_lang: Option = None; let mut code_block_content = String::new(); + // Image alt text accumulation state + let mut image_alt_content: Option = None; + let mut image_attrs: Option<(String, String)> = None; // (src, title) + for event in parser { match event { Event::Start(Tag::CodeBlock(kind)) => { @@ -33,14 +37,14 @@ pub fn markdown_to_html(markdown: &str) -> String { }; code_block_content.clear(); } - Event::Text(text) if code_block_lang.is_some() || !code_block_content.is_empty() => { + Event::Text(text) if code_block_lang.is_some() => { // Accumulate code block content - // Note: we're in a code block if we have a lang OR we've started accumulating - if code_block_lang.is_some() { - code_block_content.push_str(&text); - } else { - // Regular text, render normally - html_output.push_str(&html_escape(&text)); + code_block_content.push_str(&text); + } + Event::Text(text) if image_alt_content.is_some() => { + // Accumulate image alt text + if let Some(ref mut alt) = image_alt_content { + alt.push_str(&text); } } Event::End(TagEnd::CodeBlock) => { @@ -96,9 +100,36 @@ pub fn markdown_to_html(markdown: &str) -> String { html_output.push_str(&html_escape(&text)); html_output.push_str(""); } + Event::Start(Tag::Image { + dest_url, title, .. + }) => { + // Begin accumulating alt text; defer rendering to End event + image_alt_content = Some(String::new()); + image_attrs = Some((dest_url.to_string(), title.to_string())); + } Event::Start(tag) => { html_output.push_str(&start_tag_to_html(&tag)); } + Event::End(TagEnd::Image) => { + // Render image with accumulated alt text + let alt = image_alt_content.take().unwrap_or_default(); + if let Some((src, title)) = image_attrs.take() { + if title.is_empty() { + html_output.push_str(&format!( + "\"{}\"", + src, + html_escape(&alt) + )); + } else { + html_output.push_str(&format!( + "\"{}\"", + src, + html_escape(&alt), + html_escape(&title) + )); + } + } + } Event::End(tag) => { html_output.push_str(&end_tag_to_html(&tag)); } @@ -191,11 +222,7 @@ fn start_tag_to_html(tag: &Tag) -> String { format!("", dest_url, title) } } - Tag::Image { - dest_url, title, .. - } => { - format!("\"\"", dest_url, title) - } + Tag::Image { .. } => String::new(), // Handled separately in main loop Tag::HtmlBlock => String::new(), Tag::MetadataBlock(_) => String::new(), Tag::DefinitionListTitle => "
".to_string(), @@ -227,7 +254,7 @@ fn end_tag_to_html(tag: &TagEnd) -> String { TagEnd::Strong => "".to_string(), TagEnd::Strikethrough => "".to_string(), TagEnd::Link => "".to_string(), - TagEnd::Image => String::new(), + TagEnd::Image => String::new(), // Handled separately in main loop TagEnd::HtmlBlock => String::new(), TagEnd::MetadataBlock(_) => String::new(), TagEnd::DefinitionListTitle => "
\n".to_string(), @@ -278,4 +305,24 @@ mod tests { assert!(html.contains("cargo run")); } + + #[test] + fn test_image_alt_text() { + let md = "![Beautiful sunset](sunset.jpg \"Evening sky\")"; + let html = markdown_to_html(md); + + assert!(html.contains("alt=\"Beautiful sunset\"")); + assert!(html.contains("title=\"Evening sky\"")); + assert!(html.contains("src=\"sunset.jpg\"")); + } + + #[test] + fn test_image_alt_text_no_title() { + let md = "![Logo image](logo.png)"; + let html = markdown_to_html(md); + + assert!(html.contains("alt=\"Logo image\"")); + assert!(html.contains("src=\"logo.png\"")); + assert!(!html.contains("title=")); + } }