#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TagFlags(u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScopeKind {
Default,
ListItem,
Button,
Table,
Select,
}
impl TagFlags {
const VOID: u32 = 1 << 0;
const CLOSES_P: u32 = 1 << 1;
const P: u32 = 1 << 2;
const BUTTON: u32 = 1 << 3;
const LI: u32 = 1 << 4;
const DT_DD: u32 = 1 << 5;
const OPTION: u32 = 1 << 6;
const OPTGROUP: u32 = 1 << 7;
const TR: u32 = 1 << 8;
const CELL: u32 = 1 << 9;
const TABLE_SCOPE: u32 = 1 << 10;
const DEFAULT_BARRIER: u32 = 1 << 11;
const LIST_BARRIER: u32 = 1 << 12;
const TABLE_BARRIER: u32 = 1 << 13;
const HTML_TEMPLATE: u32 = 1 << 14;
const RAW_SCRIPT: u32 = 1 << 15;
const RAW_STYLE: u32 = 1 << 16;
const RAW_TEXTAREA: u32 = 1 << 17;
const RAW_TITLE: u32 = 1 << 18;
pub(crate) const P_MASK: Self = Self(Self::P);
pub(crate) const BUTTON_MASK: Self = Self(Self::BUTTON);
pub(crate) const LI_MASK: Self = Self(Self::LI);
pub(crate) const DT_DD_MASK: Self = Self(Self::DT_DD);
pub(crate) const OPTION_MASK: Self = Self(Self::OPTION);
pub(crate) const OPTGROUP_MASK: Self = Self(Self::OPTGROUP);
pub(crate) const TR_MASK: Self = Self(Self::TR);
pub(crate) const CELL_MASK: Self = Self(Self::CELL);
#[inline]
pub fn classify(name: &str) -> Self {
let flags = Self::classify_lowercase(name);
if flags.0 != 0 || !name.as_bytes().iter().any(u8::is_ascii_uppercase) {
return flags;
}
if name.len() > 10 {
return Self::default();
}
let mut lowercase = [0_u8; 10];
for (output, input) in lowercase.iter_mut().zip(name.bytes()) {
*output = input.to_ascii_lowercase();
}
let lowercase = unsafe { std::str::from_utf8_unchecked(&lowercase[..name.len()]) };
Self::classify_lowercase(lowercase)
}
#[inline]
fn classify_lowercase(name: &str) -> Self {
let flags = match name {
"area" | "base" | "br" | "col" | "embed" | "img" | "input" | "link" | "meta"
| "param" | "source" | "track" | "wbr" => Self::VOID,
"hr" => Self::VOID | Self::CLOSES_P,
"address" | "article" | "aside" | "blockquote" | "div" | "dl" | "fieldset"
| "footer" | "form" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "header" | "main"
| "nav" | "pre" | "section" => Self::CLOSES_P,
"p" => Self::CLOSES_P | Self::P,
"ol" | "ul" => Self::CLOSES_P | Self::LIST_BARRIER,
"table" => Self::CLOSES_P | Self::DEFAULT_BARRIER | Self::TABLE_BARRIER,
"button" => Self::BUTTON,
"li" => Self::LI,
"dt" | "dd" => Self::DT_DD,
"option" => Self::OPTION,
"optgroup" => Self::OPTGROUP,
"tr" => Self::TR | Self::TABLE_SCOPE,
"td" | "th" => Self::CELL | Self::DEFAULT_BARRIER,
"thead" | "tbody" | "tfoot" | "caption" | "colgroup" => Self::TABLE_SCOPE,
"applet" | "marquee" | "object" => Self::DEFAULT_BARRIER,
"html" | "template" => Self::HTML_TEMPLATE | Self::TABLE_BARRIER,
"script" => Self::RAW_SCRIPT,
"style" => Self::RAW_STYLE,
"textarea" => Self::RAW_TEXTAREA,
"title" => Self::RAW_TITLE,
_ => 0,
};
Self(flags)
}
#[inline]
pub(crate) const fn intersects(self, other: Self) -> bool {
self.0 & other.0 != 0
}
#[inline]
pub const fn is_void(self) -> bool {
self.0 & Self::VOID != 0
}
#[inline]
pub const fn closes_open_p(self) -> bool {
self.0 & Self::CLOSES_P != 0
}
#[inline]
pub(crate) const fn close_scope(self) -> ScopeKind {
if self.0 & (Self::LI | Self::DT_DD) != 0 {
ScopeKind::ListItem
} else if self.0 & Self::BUTTON != 0 {
ScopeKind::Button
} else if self.0 & (Self::TR | Self::CELL | Self::TABLE_SCOPE) != 0 {
ScopeKind::Table
} else if self.0 & (Self::OPTION | Self::OPTGROUP) != 0 {
ScopeKind::Select
} else {
ScopeKind::Default
}
}
#[inline]
pub const fn is_scope_barrier(self, scope: ScopeKind) -> bool {
if self.0 & Self::HTML_TEMPLATE != 0 {
return true;
}
match scope {
ScopeKind::Default => self.0 & Self::DEFAULT_BARRIER != 0,
ScopeKind::ListItem => self.0 & (Self::DEFAULT_BARRIER | Self::LIST_BARRIER) != 0,
ScopeKind::Button => self.0 & (Self::DEFAULT_BARRIER | Self::BUTTON) != 0,
ScopeKind::Table => self.0 & Self::TABLE_BARRIER != 0,
ScopeKind::Select => self.0 & (Self::OPTION | Self::OPTGROUP) == 0,
}
}
#[inline]
pub(crate) const fn raw_text_close_tag(self) -> Option<&'static str> {
if self.0 & Self::RAW_SCRIPT != 0 {
Some("</script")
} else if self.0 & Self::RAW_STYLE != 0 {
Some("</style")
} else if self.0 & Self::RAW_TEXTAREA != 0 {
Some("</textarea")
} else if self.0 & Self::RAW_TITLE != 0 {
Some("</title")
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::TagFlags;
#[test]
fn mixed_case_names_have_the_same_classification() {
for (lowercase, mixed_case) in [
("div", "DiV"),
("hr", "HR"),
("button", "BuTtOn"),
("optgroup", "OPTGROUP"),
("colgroup", "ColGroup"),
("template", "TEMPLATE"),
("textarea", "TextArea"),
] {
assert_eq!(
TagFlags::classify(lowercase),
TagFlags::classify(mixed_case)
);
}
}
#[test]
fn unknown_names_have_no_flags() {
assert_eq!(TagFlags::classify("custom-element"), TagFlags::default());
assert_eq!(TagFlags::classify("CUSTOM-ELEMENT"), TagFlags::default());
}
}