use crate::error::Result;
pub fn extract_markdown(html: &str) -> Result<String> {
let mut md = String::new();
let mut in_tag = false;
let mut tag_name = String::new();
let mut in_script = false;
let mut in_style = false;
let mut pending_prefix: Option<String> = None;
let chars: Vec<char> = html.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '<' {
in_tag = true;
tag_name.clear();
i += 1;
continue;
}
if c == '>' && in_tag {
in_tag = false;
let tag = tag_name.trim().to_lowercase();
if tag == "script" || tag.starts_with("script ") {
in_script = true;
} else if tag == "/script" {
in_script = false;
} else if tag == "style" || tag.starts_with("style ") {
in_style = true;
} else if tag == "/style" {
in_style = false;
}
if tag == "h1" || tag.starts_with("h1 ") {
pending_prefix = Some("\n# ".to_string());
} else if tag == "h2" || tag.starts_with("h2 ") {
pending_prefix = Some("\n## ".to_string());
} else if tag == "h3" || tag.starts_with("h3 ") {
pending_prefix = Some("\n### ".to_string());
} else if tag == "h4" || tag.starts_with("h4 ") {
pending_prefix = Some("\n#### ".to_string());
} else if tag == "p" || tag.starts_with("p ") {
pending_prefix = Some("\n\n".to_string());
} else if tag == "li" || tag.starts_with("li ") {
pending_prefix = Some("\n- ".to_string());
} else if tag == "blockquote" || tag.starts_with("blockquote ") {
pending_prefix = Some("\n> ".to_string());
} else if tag == "pre" || tag.starts_with("pre ") {
pending_prefix = Some("\n```\n".to_string());
} else if tag == "strong" || tag == "b" {
pending_prefix = Some("**".to_string());
} else if tag == "em" || tag == "i" {
pending_prefix = Some("*".to_string());
} else if tag == "code" && !in_pre(&md) {
pending_prefix = Some("`".to_string());
} else if tag.starts_with("a ") {
if let Some(href) = extract_attr(&tag, "href") {
pending_prefix = Some(format!("[{}](", href));
}
}
if tag == "br" || tag == "br/" || tag.starts_with("br ") {
md.push('\n');
} else if tag == "hr" || tag.starts_with("hr ") {
md.push_str("\n---\n");
} else if tag == "/pre" || tag.starts_with("pre ") {
md.push_str("\n```\n");
} else if tag == "/code" && !in_pre(&md) {
md.push('`');
} else if tag == "/a" || tag.starts_with("a ") {
md.push(')');
} else if tag == "/strong" || tag == "/b" {
md.push_str("**");
} else if tag == "/em" || tag == "/i" {
md.push('*');
} else if tag == "/ul" || tag == "/ol" {
md.push('\n');
} else if tag == "/li" || tag.starts_with("li ") {
md.push('\n');
} else if tag == "/blockquote" || tag.starts_with("blockquote ") {
md.push('\n');
}
i += 1;
continue;
}
if in_tag {
if c.is_whitespace() && tag_name.is_empty() {
} else {
tag_name.push(c);
}
i += 1;
continue;
}
if in_script || in_style {
i += 1;
continue;
}
if !in_tag {
if let Some(prefix) = pending_prefix.take() {
md.push_str(&prefix);
}
if c.is_whitespace() {
if md.ends_with(' ') || md.ends_with('\n') {
} else {
md.push(' ');
}
} else {
md.push(c);
}
}
i += 1;
}
let md = md.replace(" ", " ");
let md = md.lines().map(|l| l.trim_end()).collect::<Vec<_>>().join("\n");
let md = md.replace("\n\n\n", "\n\n");
Ok(md.trim().to_string())
}
fn in_pre(md: &str) -> bool {
let pre_count = md.matches("\n```\n").count();
pre_count % 2 == 1
}
fn extract_attr(tag: &str, attr: &str) -> Option<String> {
let pattern = format!("{}=\"", attr);
let start = tag.find(&pattern)? + pattern.len();
let end = tag[start..].find('"')? + start;
Some(tag[start..end].to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_markdown_basic() {
let html = "<h1>Title</h1><p>Hello world</p>";
let md = extract_markdown(html).unwrap();
assert!(md.contains("# Title"));
assert!(md.contains("Hello world"));
}
#[test]
fn test_extract_markdown_headers() {
let html = "<h2>Section</h2><h3>Subsection</h3>";
let md = extract_markdown(html).unwrap();
assert!(md.contains("## Section"));
assert!(md.contains("### Subsection"));
}
#[test]
fn test_extract_markdown_links() {
let html = r#"<a href="https://example.com">Link text</a>"#;
let md = extract_markdown(html).unwrap();
assert!(md.contains("[https://example.com]("));
}
#[test]
fn test_extract_markdown_skips_script() {
let html = "<p>Hello</p><script>alert('x')</script><p>World</p>";
let md = extract_markdown(html).unwrap();
assert!(!md.contains("alert"));
assert!(md.contains("Hello"));
assert!(md.contains("World"));
}
#[test]
fn test_extract_markdown_list() {
let html = "<ul><li>Item 1</li><li>Item 2</li></ul>";
let md = extract_markdown(html).unwrap();
assert!(md.contains("- Item 1"));
assert!(md.contains("- Item 2"));
}
#[test]
fn test_extract_markdown_bold() {
let html = "<p><strong>Bold</strong> and <b>also</b></p>";
let md = extract_markdown(html).unwrap();
assert!(md.contains("**Bold**"));
assert!(md.contains("**also**"));
}
}