pub(crate) fn has_valid_alt(tag: &str) -> bool {
let has_alt_eq = tag.contains("alt=");
let has_alt_bare = !has_alt_eq
&& (tag.contains(" alt ")
|| tag.contains(" alt>")
|| tag.ends_with(" alt"));
has_alt_eq || has_alt_bare
}
pub(crate) fn has_empty_alt(tag: &str) -> bool {
let has_alt_eq = tag.contains("alt=");
let has_alt_bare = !has_alt_eq
&& (tag.contains(" alt ")
|| tag.contains(" alt>")
|| tag.ends_with(" alt"));
tag.contains("alt=\"\"")
|| tag.contains("alt=''")
|| has_alt_bare
|| (has_alt_eq && !tag.contains("alt=\"") && !tag.contains("alt='"))
}
pub(crate) fn is_decorative_img(tag: &str) -> bool {
tag.contains("role=\"presentation\"")
|| tag.contains("role=\"none\"")
|| tag.contains("role='presentation'")
|| tag.contains("role='none'")
|| tag.contains("role=presentation")
|| tag.contains("role=none")
}
pub const fn find_tag_end(html: &str, tag_start: usize) -> usize {
let bytes = html.as_bytes();
let mut i = tag_start;
let mut quote: Option<u8> = None;
while i < bytes.len() {
let b = bytes[i];
match quote {
Some(q) if b == q => quote = None,
Some(_) => {}
None => match b {
b'"' | b'\'' => quote = Some(b),
b'>' => return i + 1,
_ => {}
},
}
i += 1;
}
bytes.len()
}
pub(crate) fn extract_attr_value(tag: &str, attr: &str) -> Option<String> {
let lower = tag.to_ascii_lowercase();
let pattern = format!("{attr}=");
let start = lower.find(&pattern)?;
let after = &tag[start + pattern.len()..];
let trimmed = after.trim_start();
if let Some(inner) = trimmed.strip_prefix('"') {
let end = inner.find('"')?;
Some(inner[..end].to_string())
} else if let Some(inner) = trimmed.strip_prefix('\'') {
let end = inner.find('\'')?;
Some(inner[..end].to_string())
} else {
let end = trimmed
.find(|c: char| c.is_whitespace() || c == '>')
.unwrap_or(trimmed.len());
Some(trimmed[..end].to_string())
}
}
pub(crate) fn strip_tags_simple(html: &str) -> String {
let mut result = String::with_capacity(html.len());
let mut in_tag = false;
for ch in html.chars() {
if ch == '<' {
in_tag = true;
} else if ch == '>' {
in_tag = false;
} else if !in_tag {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_attr_value_double_quoted() {
let result = extract_attr_value(r#"<a href="/foo">"#, "href");
assert_eq!(result, Some("/foo".to_string()));
}
#[test]
fn extract_attr_value_single_quoted() {
let result = extract_attr_value(r"<a href='/bar'>", "href");
assert_eq!(result, Some("/bar".to_string()));
}
#[test]
fn extract_attr_value_unquoted() {
let result = extract_attr_value(r"<a href=/baz>", "href");
assert_eq!(result, Some("/baz".to_string()));
}
#[test]
fn extract_attr_value_missing_attribute_returns_none() {
let result = extract_attr_value(r"<a>", "href");
assert!(result.is_none());
}
#[test]
fn strip_tags_simple_removes_html_tags_and_preserves_text() {
let result = strip_tags_simple("<p>hello <b>world</b>!</p>");
assert_eq!(result, "hello world!");
}
#[test]
fn strip_tags_simple_handles_empty_and_text_only() {
assert_eq!(strip_tags_simple(""), "");
assert_eq!(strip_tags_simple("plain text"), "plain text");
}
#[test]
fn has_empty_alt_detects_bare_alt_attribute() {
assert!(has_empty_alt("<img src=x alt>"));
assert!(has_empty_alt("<img alt src=x>"));
assert!(has_empty_alt("<img src=x alt"));
}
#[test]
fn has_empty_alt_detects_unquoted_missing_value() {
assert!(has_empty_alt("<img alt=>"));
assert!(!has_empty_alt("<img alt='photo'>"));
}
#[test]
fn is_decorative_img_covers_all_role_spellings() {
assert!(is_decorative_img("<img role=\"presentation\">"));
assert!(is_decorative_img("<img role=\"none\">"));
assert!(is_decorative_img("<img role='presentation'>"));
assert!(is_decorative_img("<img role='none'>"));
assert!(is_decorative_img("<img role=presentation>"));
assert!(is_decorative_img("<img role=none>"));
assert!(!is_decorative_img("<img role=\"img\">"));
}
#[test]
fn find_tag_end_without_closing_bracket_returns_len() {
let html = "<img src=\"unterminated";
assert_eq!(find_tag_end(html, 0), html.len());
}
#[test]
fn test_extract_attr_value_quoting_styles() {
assert_eq!(
extract_attr_value("<img alt=\"hello\">", "alt"),
Some("hello".to_string())
);
assert_eq!(
extract_attr_value("<img alt='single'>", "alt"),
Some("single".to_string())
);
assert_eq!(
extract_attr_value("<img alt=unquoted>", "alt"),
Some("unquoted".to_string())
);
assert_eq!(
extract_attr_value("<img alt=unquoted-space class=x>", "alt"),
Some("unquoted-space".to_string())
);
assert_eq!(extract_attr_value("<img alt=\"unclosed>", "alt"), None);
assert_eq!(extract_attr_value("<img alt='unclosed>", "alt"), None);
assert_eq!(extract_attr_value("<img alt=\"hello\">", "width"), None);
}
}