");
html_output.push_str(&html_escape(&code_block_content));
html_output.push_str("\n");
}
}
} else {
// Code blocks: syntax highlighting
html_output.push_str("", lang_str));
html_output.push_str(&highlight_code(lang, &code_block_content));
} else {
// Unsupported language: render as plain escaped text
if !lang_str.is_empty() {
html_output.push_str(&format!(" class=\"language-{}\">", lang_str));
} else {
html_output.push('>');
}
html_output.push_str(&html_escape(&code_block_content));
}
html_output.push_str("\n");
}
code_block_lang = None;
code_block_content.clear();
}
Event::Text(text) if heading_level.is_some() => {
// Accumulate heading text
heading_text.push_str(&text);
html_output.push_str(&html_escape(&text));
}
Event::Text(text) => {
// Regular text outside code blocks
html_output.push_str(&html_escape(&text));
}
Event::Code(text) => {
// Inline code
html_output.push_str("");
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::Heading { level, .. }) => {
// Begin accumulating heading text
heading_level = Some(level);
heading_text.clear();
let level_num = level as u8;
html_output.push_str(&format!("");
html_output.push_str(&html_escape(&latex));
html_output.push_str("");
}
},
Event::DisplayMath(latex) => match crate::math::render_math(&latex, true) {
Ok(rendered) => {
html_output.push_str("");
html_output.push_str(&html_escape(&latex));
html_output.push_str("\n");
}
},
}
}
(html_output, anchors)
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
/// Convert heading text to a URL-friendly slug ID.
fn slugify(text: &str) -> String {
text.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::".to_string(),
Tag::Heading { level, .. } => format!("\n".to_string(),
Tag::CodeBlock(_) => String::new(), // Handled separately
Tag::List(Some(start)) => format!("
\n", start),
Tag::List(None) => "
\n".to_string(),
Tag::Item => "
Hello¶
"
));
assert!(html.contains("test"));
}
#[test]
fn test_code_block_highlighting() {
let md = "```rust\nfn main() {}\n```";
let (html, _) = markdown_to_html(md);
// Should contain highlighted code
assert!(html.contains("cargo run"));
}
#[test]
fn test_image_alt_text() {
let md = "";
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 = "";
let (html, _) = markdown_to_html(md);
assert!(html.contains("alt=\"Logo image\""));
assert!(html.contains("src=\"logo.png\""));
assert!(!html.contains("title="));
}
#[test]
fn test_anchor_extraction() {
let md = r#"# Page Title
## Getting Started
Some intro text.
### Installation
Install steps.
## Configuration
Config details.
#### Deep Heading
"#;
let (html, anchors) = markdown_to_html(md);
// h1 should NOT be extracted (page title, not TOC)
assert!(anchors.iter().all(|a| a.level >= 2));
// Should have 4 anchors: h2, h3, h2, h4
assert_eq!(anchors.len(), 4);
// Check first anchor
assert_eq!(anchors[0].id, "getting-started");
assert_eq!(anchors[0].label, "Getting Started");
assert_eq!(anchors[0].level, 2);
// Check h3
assert_eq!(anchors[1].id, "installation");
assert_eq!(anchors[1].level, 3);
// Check second h2
assert_eq!(anchors[2].id, "configuration");
assert_eq!(anchors[2].level, 2);
// Check h4
assert_eq!(anchors[3].id, "deep-heading");
assert_eq!(anchors[3].level, 4);
// Verify IDs are in HTML
assert!(html.contains("id=\"getting-started\""));
assert!(html.contains("id=\"installation\""));
}
#[test]
fn test_slugify_edge_cases() {
// Basic case
assert_eq!(slugify("Hello World"), "hello-world");
// Multiple spaces → single hyphen
assert_eq!(slugify("Hello World"), "hello-world");
// Special characters → hyphen (apostrophe becomes hyphen)
assert_eq!(slugify("What's New?"), "what-s-new");
// Numbers preserved, dot becomes hyphen
assert_eq!(slugify("Version 2.0"), "version-2-0");
// Leading/trailing spaces trimmed
assert_eq!(slugify(" Padded "), "padded");
// Mixed case → lowercase
assert_eq!(slugify("CamelCase"), "camelcase");
// Consecutive special chars → single hyphen
assert_eq!(slugify("A -- B"), "a-b");
}
}