use regex::Regex;
use std::sync::LazyLock;
use vtcode_commons::formatting::collapse_whitespace;
use super::classify_helpers::decode_html_entities;
static STRIP_UNREADABLE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?is)<script[^>]*>.*?</script>|<style[^>]*>.*?</style>|<noscript[^>]*>.*?</noscript>|<template[^>]*>.*?</template>|<svg[^>]*>.*?</svg>",
)
.expect("valid unreadable-block regex")
});
static HTML_COMMENT: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?s)<!--.*?-->").expect("valid html comment regex"));
static HTML_HEAD: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?is)<head[^>]*>.*?</head>").expect("valid html head regex"));
static HTML_TITLE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?is)<title[^>]*>(?P<title>.*?)</title>").expect("valid html title regex"));
static HTML_TAG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]+>").expect("valid html tag regex"));
pub(super) fn looks_like_html(content: &str) -> bool {
let trimmed = content.trim_start().trim_start_matches('\u{FEFF}');
trimmed.as_bytes().first() == Some(&b'<') && trimmed.contains('>')
}
pub(super) fn extract_text_preview(html: &str) -> String {
let title = HTML_TITLE
.captures(html)
.and_then(|caps| caps.name("title"))
.map(|found| clean_fragment(found.as_str()))
.unwrap_or_default();
let without_blocks = STRIP_UNREADABLE.replace_all(html, " ");
let without_comments = HTML_COMMENT.replace_all(&without_blocks, " ");
let without_head = HTML_HEAD.replace_all(&without_comments, " ");
let body = clean_fragment(&without_head);
if title.is_empty() {
body
} else if body.is_empty() {
title
} else {
format!("{title}\n\n{body}")
}
}
fn clean_fragment(fragment: &str) -> String {
let without_tags = HTML_TAG.replace_all(fragment, " ");
collapse_whitespace(&decode_html_entities(without_tags.trim()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_unreadable_blocks_and_comments_but_keeps_title_and_body() {
let html = "<!doctype html><html><head><title>Example & Co</title>\
<style>.x{color:red}</style><script>alert(1)</script></head>\
<body><!-- nav --><nav><a href=\"/\">Home</a></nav>\
<main><h1>Hello A</h1><p>World</p></main>\
<svg><circle cx=\"1\"/></svg></body></html>";
let text = extract_text_preview(html);
assert!(text.starts_with("Example & Co"), "title first; got: {text}");
assert!(text.contains("Hello A"), "entity decoded; got: {text}");
assert!(text.contains("World"), "body kept; got: {text}");
assert!(!text.contains("alert"), "script stripped; got: {text}");
assert!(!text.contains("color:red"), "style stripped; got: {text}");
assert!(!text.contains("nav"), "comment stripped; got: {text}");
assert!(!text.contains('<'), "no tags remain; got: {text}");
}
#[test]
fn style_after_content_does_not_eat_body() {
let html = "<div><p>Keep me</p></div><style>.late{display:none}</style><p>And me</p>";
let text = extract_text_preview(html);
assert!(text.contains("Keep me"), "got: {text}");
assert!(text.contains("And me"), "got: {text}");
assert!(!text.contains("display"), "got: {text}");
}
#[test]
fn title_only_page_returns_title() {
let html = "<html><head><title>Lonely</title></head><body><script>var x = 1;</script></body></html>";
assert_eq!(extract_text_preview(html), "Lonely");
}
#[test]
fn looks_like_html_gates_on_leading_angle_bracket() {
assert!(looks_like_html("<!doctype html><html></html>"));
assert!(looks_like_html(" \n\t<html><body>hi</body></html>"));
assert!(looks_like_html("\u{FEFF}<html></html>"));
assert!(!looks_like_html("{\"key\": \"value > 1\"}"));
assert!(!looks_like_html("# Markdown docs"));
assert!(!looks_like_html(""));
assert!(!looks_like_html(" "));
}
}