#![allow(clippy::significant_drop_tightening)]
#![cfg(feature = "visitor")]
use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult, VisitorHandle};
use html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};
use std::sync::{Arc, Mutex};
fn convert(
html: &str,
options: Option<ConversionOptions>,
visitor: Option<VisitorHandle>,
) -> Result<ConversionResult, ConversionError> {
let mut opts = options.unwrap_or_default();
if visitor.is_some() {
opts.visitor = visitor;
}
html_to_markdown_rs::convert(html, Some(opts))
}
#[allow(dead_code)]
#[derive(Debug, Default)]
struct DocumentedButBrokenVisitor {
skipped_elements: Vec<(String, String)>,
}
#[derive(Debug, Default)]
struct ContentFilter {
skipped_elements: Vec<(String, String)>,
}
impl HtmlVisitor for ContentFilter {
fn visit_element_start(&mut self, ctx: &NodeContext<'_>) -> VisitResult {
let tag_name: &str = ctx.tag_name.as_ref();
match tag_name {
"div" => {
let classes = ctx.attributes().get("class").map_or("", std::string::String::as_str);
if classes.contains("ad")
|| classes.contains("advertisement")
|| classes.contains("tracking")
|| classes.contains("analytics")
{
self.skipped_elements.push(("div".to_string(), classes.to_string()));
return VisitResult::Skip;
}
}
"script" => {
self.skipped_elements.push(("script".to_string(), String::new()));
return VisitResult::Skip;
}
"style" => {
self.skipped_elements.push(("style".to_string(), String::new()));
return VisitResult::Skip;
}
_ => {}
}
VisitResult::Continue
}
fn visit_image(&mut self, ctx: &NodeContext, src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
let width = ctx.attributes().get("width").map_or("", std::string::String::as_str);
let height = ctx.attributes().get("height").map_or("", std::string::String::as_str);
if width == "1" && height == "1" {
self.skipped_elements
.push(("img".to_string(), format!("tracking pixel: {src}")));
return VisitResult::Skip;
}
if src.to_lowercase().contains("tracking") || src.to_lowercase().contains("analytics") {
self.skipped_elements
.push(("img".to_string(), format!("tracking URL: {src}")));
return VisitResult::Skip;
}
VisitResult::Continue
}
fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {
if href.to_lowercase().contains("utm_") {
if let Some(base_url) = href.split('?').next() {
return VisitResult::Custom(format!("[{text}]({base_url})"));
}
}
VisitResult::Continue
}
}
#[test]
fn test_issue_187_content_filter() {
let html = r#"
<article>
<h1>Blog Post Title</h1>
<p>This is the main content of the article.</p>
<div class="ad advertisement">
<p>This is an advertisement block that should be removed.</p>
</div>
<p>More content here.</p>
<img src="https://tracking.example.com/pixel.gif" width="1" height="1" alt="">
<div class="content">
<p>Legitimate content in a div.</p>
<img src="https://cdn.example.com/image.jpg" alt="Article image" width="800">
</div>
<script>
console.log("This script should be removed");
</script>
<p>Read more on <a href="https://example.com/article?utm_source=newsletter&utm_medium=email">our website</a>.</p>
<div class="tracking analytics">
<img src="https://analytics.example.com/track.png" alt="">
</div>
</article>
"#;
let visitor = Arc::new(Mutex::new(ContentFilter::default()));
let result = convert(html, None, Some(visitor.clone()))
.unwrap()
.content
.unwrap_or_default();
println!("Converted Markdown:\n{result}");
println!("\nSkipped Elements:");
for (tag, info) in &visitor.lock().expect("visitor mutex poisoned").skipped_elements {
println!("- {tag}: {info}");
}
assert!(
!result.contains("advertisement block that should be removed"),
"Ad div should be removed"
);
assert!(!result.contains("console.log"), "Script tag should be removed");
assert!(result.contains("Blog Post Title"), "Heading should be preserved");
assert!(result.contains("main content"), "Main content should be preserved");
assert!(result.contains("Legitimate content"), "Content div should be preserved");
assert!(result.contains("Article image"), "Legitimate image should be preserved");
assert!(
result.contains("[our website](https://example.com/article)"),
"Link tracking params should be stripped"
);
let borrowed = visitor.lock().expect("visitor mutex poisoned");
assert!(
borrowed.skipped_elements.iter().any(|(tag, _)| tag == "div"),
"Should have skipped ad divs"
);
assert!(
borrowed.skipped_elements.iter().any(|(tag, _)| tag == "img"),
"Should have skipped tracking pixel"
);
}