pub const TYPE_1_BLOCK_ELEMENTS: &[&str] = &["pre", "script", "style", "textarea"];
pub const BLOCK_ELEMENTS: &[&str] = &[
"address",
"article",
"aside",
"audio",
"blockquote",
"canvas",
"details",
"dialog",
"dd",
"div",
"dl",
"dt",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"iframe",
"li",
"main",
"menu",
"nav",
"noscript",
"object",
"ol",
"p",
"picture",
"pre",
"script",
"search",
"section",
"source",
"style",
"summary",
"svg",
"table",
"tbody",
"td",
"template",
"textarea",
"tfoot",
"th",
"thead",
"tr",
"track",
"ul",
"video",
];
pub fn parse_html_block_start(trimmed: &str) -> Option<(String, bool)> {
let after_bracket = trimmed.strip_prefix('<')?;
if after_bracket.is_empty() {
return None;
}
let is_closing = after_bracket.starts_with('/');
let tag_start = if is_closing { &after_bracket[1..] } else { after_bracket };
let tag_name = tag_start
.chars()
.take_while(|c| c.is_ascii_alphabetic() || *c == '-' || c.is_ascii_digit())
.collect::<String>()
.to_lowercase();
let rest = &tag_start[tag_name.len()..];
let terminated =
rest.is_empty() || rest.starts_with(|c: char| c.is_ascii_whitespace() || c == '>') || rest.starts_with("/>");
if terminated && !tag_name.is_empty() && BLOCK_ELEMENTS.contains(&tag_name.as_str()) {
Some((tag_name, is_closing))
} else {
None
}
}
pub fn opens_untagged_html_block(trimmed: &str) -> bool {
let Some(after_bracket) = trimmed.strip_prefix('<') else {
return false;
};
after_bracket.starts_with("!--")
|| after_bracket.starts_with('?')
|| after_bracket.starts_with("![CDATA[")
|| after_bracket
.strip_prefix('!')
.is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_alphabetic()))
}
#[cfg(test)]
mod tests {
use super::{opens_untagged_html_block, parse_html_block_start};
#[test]
fn untagged_html_block_openers_are_the_spec_start_conditions() {
for (line, expected) in [
("<!-- note -->", true),
("<!--", true),
("<?php echo 1; ?>", true),
("<!DOCTYPE html>", true),
("<![CDATA[x]]>", true),
("<!>", false),
("<!1>", false),
("<![cdata[x]]>", false),
("<div>", false),
("text <!-- note -->", false),
("", false),
] {
assert_eq!(opens_untagged_html_block(line), expected, "{line:?}");
}
}
#[test]
fn a_block_tag_name_needs_a_terminator() {
for (line, expected) in [
("<div>", Some(("div".to_string(), false))),
("<div class=\"x\">", Some(("div".to_string(), false))),
("<div", Some(("div".to_string(), false))),
("<div/>", Some(("div".to_string(), false))),
("<DIV\tid=x>", Some(("div".to_string(), false))),
("</div>", Some(("div".to_string(), true))),
("</div", Some(("div".to_string(), true))),
("<pre>", Some(("pre".to_string(), false))),
("<h1>", Some(("h1".to_string(), false))),
("<div.class>", None),
("<p,", None),
("<div/x>", None),
("<div=1>", None),
("<span>", None),
("<div-custom>", None),
("<h1foo>", None),
("<", None),
("</", None),
("text <div>", None),
] {
assert_eq!(parse_html_block_start(line), expected, "{line:?}");
}
}
}