1pub const TYPE_1_BLOCK_ELEMENTS: &[&str] = &["pre", "script", "style", "textarea"];
12
13pub const BLOCK_ELEMENTS: &[&str] = &[
16 "address",
17 "article",
18 "aside",
19 "audio",
20 "blockquote",
21 "canvas",
22 "details",
23 "dialog",
24 "dd",
25 "div",
26 "dl",
27 "dt",
28 "embed",
29 "fieldset",
30 "figcaption",
31 "figure",
32 "footer",
33 "form",
34 "h1",
35 "h2",
36 "h3",
37 "h4",
38 "h5",
39 "h6",
40 "header",
41 "hr",
42 "iframe",
43 "li",
44 "main",
45 "menu",
46 "nav",
47 "noscript",
48 "object",
49 "ol",
50 "p",
51 "picture",
52 "pre",
53 "script",
54 "search",
55 "section",
56 "source",
57 "style",
58 "summary",
59 "svg",
60 "table",
61 "tbody",
62 "td",
63 "template",
64 "textarea",
65 "tfoot",
66 "th",
67 "thead",
68 "tr",
69 "track",
70 "ul",
71 "video",
72];
73
74pub fn parse_html_block_start(trimmed: &str) -> Option<(String, bool)> {
83 let after_bracket = trimmed.strip_prefix('<')?;
84 if after_bracket.is_empty() {
85 return None;
86 }
87 let is_closing = after_bracket.starts_with('/');
88 let tag_start = if is_closing { &after_bracket[1..] } else { after_bracket };
89
90 let tag_name = tag_start
91 .chars()
92 .take_while(|c| c.is_ascii_alphabetic() || *c == '-' || c.is_ascii_digit())
93 .collect::<String>()
94 .to_lowercase();
95
96 let rest = &tag_start[tag_name.len()..];
97 let terminated =
98 rest.is_empty() || rest.starts_with(|c: char| c.is_ascii_whitespace() || c == '>') || rest.starts_with("/>");
99
100 if terminated && !tag_name.is_empty() && BLOCK_ELEMENTS.contains(&tag_name.as_str()) {
101 Some((tag_name, is_closing))
102 } else {
103 None
104 }
105}
106
107pub fn opens_untagged_html_block(trimmed: &str) -> bool {
114 let Some(after_bracket) = trimmed.strip_prefix('<') else {
115 return false;
116 };
117 after_bracket.starts_with("!--")
118 || after_bracket.starts_with('?')
119 || after_bracket.starts_with("![CDATA[")
120 || after_bracket
121 .strip_prefix('!')
122 .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_alphabetic()))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::{opens_untagged_html_block, parse_html_block_start};
128
129 #[test]
130 fn untagged_html_block_openers_are_the_spec_start_conditions() {
131 for (line, expected) in [
132 ("<!-- note -->", true),
133 ("<!--", true),
134 ("<?php echo 1; ?>", true),
135 ("<!DOCTYPE html>", true),
136 ("<![CDATA[x]]>", true),
137 ("<!>", false),
138 ("<!1>", false),
139 ("<![cdata[x]]>", false),
140 ("<div>", false),
141 ("text <!-- note -->", false),
142 ("", false),
143 ] {
144 assert_eq!(opens_untagged_html_block(line), expected, "{line:?}");
145 }
146 }
147
148 #[test]
149 fn a_block_tag_name_needs_a_terminator() {
150 for (line, expected) in [
151 ("<div>", Some(("div".to_string(), false))),
152 ("<div class=\"x\">", Some(("div".to_string(), false))),
153 ("<div", Some(("div".to_string(), false))),
154 ("<div/>", Some(("div".to_string(), false))),
155 ("<DIV\tid=x>", Some(("div".to_string(), false))),
156 ("</div>", Some(("div".to_string(), true))),
157 ("</div", Some(("div".to_string(), true))),
158 ("<pre>", Some(("pre".to_string(), false))),
159 ("<h1>", Some(("h1".to_string(), false))),
160 ("<div.class>", None),
161 ("<p,", None),
162 ("<div/x>", None),
163 ("<div=1>", None),
164 ("<span>", None),
165 ("<div-custom>", None),
166 ("<h1foo>", None),
167 ("<", None),
168 ("</", None),
169 ("text <div>", None),
170 ] {
171 assert_eq!(parse_html_block_start(line), expected, "{line:?}");
172 }
173 }
174}