pub(crate) fn is_void(name: &str) -> bool {
matches!(
name,
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}
pub(crate) fn is_raw_text(name: &str) -> bool {
matches!(
name,
"script" | "style" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext"
)
}
pub(crate) fn ends_only_at_end_tag(name: &str) -> bool {
is_raw_text(name) || matches!(name, "title" | "textarea")
}
pub(crate) fn is_valid_attr_name(name: &str) -> bool {
!name.is_empty()
&& !name.bytes().any(|b| {
b.is_ascii_whitespace() || matches!(b, b'"' | b'\'' | b'<' | b'>' | b'/' | b'=' | 0)
})
}
pub(crate) fn is_valid_tag_name(name: &str) -> bool {
name.as_bytes().first().is_some_and(u8::is_ascii_alphabetic) && is_valid_attr_name(name)
}
fn closes_p(name: &str) -> bool {
matches!(
name,
"address"
| "article"
| "aside"
| "blockquote"
| "center"
| "dd"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "dt"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "form"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "header"
| "hgroup"
| "hr"
| "li"
| "listing"
| "main"
| "menu"
| "nav"
| "ol"
| "p"
| "plaintext"
| "pre"
| "search"
| "section"
| "summary"
| "table"
| "ul"
| "xmp"
)
}
fn is_heading(name: &str) -> bool {
matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
}
fn in_head(name: &str) -> bool {
matches!(
name,
"base"
| "basefont"
| "bgsound"
| "link"
| "meta"
| "noframes"
| "noscript"
| "script"
| "style"
| "template"
| "title"
)
}
fn table_structure(name: &str) -> bool {
matches!(
name,
"caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead"
)
}
pub(crate) fn implicitly_closes(opening: &str, open: &str) -> bool {
match open {
"p" => closes_p(opening),
"li" => opening == "li",
"dt" | "dd" => matches!(opening, "dt" | "dd"),
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => is_heading(opening),
"head" => !in_head(opening),
"option" => matches!(opening, "option" | "optgroup" | "hr"),
"optgroup" => matches!(opening, "optgroup" | "hr"),
"rb" | "rt" | "rp" => matches!(opening, "rb" | "rt" | "rtc" | "rp"),
"rtc" => matches!(opening, "rb" | "rtc"),
"td" | "th" => matches!(opening, "td" | "th" | "tr") || table_structure(opening),
"tr" => opening == "tr" || table_structure(opening),
"thead" | "tbody" | "tfoot" => table_structure(opening),
"caption" => matches!(opening, "td" | "th" | "tr") || table_structure(opening),
"colgroup" => !matches!(opening, "col" | "template"),
_ => false,
}
}
pub(crate) type SearchFn = fn(&str, &str) -> Option<Search>;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Search {
Closes,
Stops,
Continue,
}
pub(crate) fn search(opening: &str, open: &str) -> Option<Search> {
let found = |closes: bool, stops: bool| {
Some(if closes {
Search::Closes
} else if stops {
Search::Stops
} else {
Search::Continue
})
};
match opening {
"li" => found(
open == "li",
is_special(open) && !matches!(open, "address" | "div" | "p"),
),
"dd" | "dt" => found(
matches!(open, "dd" | "dt"),
is_special(open) && !matches!(open, "address" | "div" | "p"),
),
"td" | "th" | "tr" => found(
implicitly_closes(opening, open),
matches!(open, "table" | "template" | "html"),
),
o if table_structure(o) => found(
implicitly_closes(opening, open),
matches!(open, "table" | "template" | "html"),
),
_ => None,
}
}
pub(crate) fn p_search(opening: &str, open: &str) -> Option<Search> {
if !closes_p(opening) {
return None;
}
Some(if open == "p" {
Search::Closes
} else if matches!(
open,
"applet"
| "button"
| "caption"
| "html"
| "marquee"
| "math"
| "object"
| "svg"
| "table"
| "td"
| "template"
| "th"
) {
Search::Stops
} else {
Search::Continue
})
}
fn is_special(name: &str) -> bool {
matches!(
name,
"address"
| "applet"
| "area"
| "article"
| "aside"
| "base"
| "basefont"
| "bgsound"
| "blockquote"
| "body"
| "br"
| "button"
| "caption"
| "center"
| "col"
| "colgroup"
| "dd"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "dt"
| "embed"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "form"
| "frame"
| "frameset"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "head"
| "header"
| "hgroup"
| "hr"
| "html"
| "iframe"
| "img"
| "input"
| "keygen"
| "li"
| "link"
| "listing"
| "main"
| "marquee"
| "menu"
| "meta"
| "nav"
| "noembed"
| "noframes"
| "noscript"
| "object"
| "ol"
| "p"
| "param"
| "plaintext"
| "pre"
| "script"
| "search"
| "section"
| "select"
| "source"
| "style"
| "summary"
| "table"
| "tbody"
| "td"
| "template"
| "textarea"
| "tfoot"
| "th"
| "thead"
| "title"
| "tr"
| "track"
| "ul"
| "wbr"
| "xmp"
| "svg"
| "math"
)
}
pub(crate) fn end_tag_optional(name: &str) -> bool {
matches!(
name,
"body"
| "caption"
| "colgroup"
| "dd"
| "dt"
| "head"
| "html"
| "li"
| "optgroup"
| "option"
| "p"
| "rb"
| "rp"
| "rt"
| "rtc"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
)
}
pub(crate) fn text_closes(open: &str) -> bool {
matches!(open, "head" | "colgroup")
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Display {
Hidden,
Inline,
Block,
Paragraph,
}
pub(crate) fn display(name: &str) -> Display {
match name {
"head" | "title" | "script" | "style" | "template" | "noscript" | "noembed" | "iframe"
| "object" | "svg" | "datalist" => Display::Hidden,
"p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote" | "pre" | "ul" | "ol"
| "menu" | "dl" | "table" | "hr" => Display::Paragraph,
"address" | "article" | "aside" | "body" | "caption" | "center" | "dd" | "details"
| "dialog" | "div" | "dt" | "fieldset" | "figcaption" | "figure" | "footer" | "form"
| "header" | "hgroup" | "html" | "legend" | "li" | "main" | "nav" | "optgroup"
| "option" | "search" | "section" | "summary" | "tbody" | "textarea" | "tfoot"
| "thead" | "tr" => Display::Block,
_ => Display::Inline,
}
}