#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct LocalNameHash(u64);
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
impl LocalNameHash {
pub const NONE: Self = Self(0);
#[must_use]
pub fn of(name: &[u8]) -> Self {
if name.is_empty() {
return Self::NONE;
}
let mut hash = FNV_OFFSET;
for &byte in name {
hash ^= u64::from(byte.to_ascii_lowercase());
hash = hash.wrapping_mul(FNV_PRIME);
}
Self(hash)
}
#[must_use]
pub const fn from_static(name: &'static [u8]) -> Self {
let mut hash = FNV_OFFSET;
let mut i = 0;
while i < name.len() {
debug_assert!(!name[i].is_ascii_uppercase());
hash ^= name[i] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
Self(hash)
}
#[must_use]
pub const fn is_none(self) -> bool {
self.0 == 0
}
pub(crate) const fn as_u64(self) -> u64 {
self.0
}
}
#[cfg(test)]
mod tests {
use ahash::HashMap;
use super::LocalNameHash;
#[test]
fn case_insensitive_and_const_agreement() {
assert_eq!(LocalNameHash::of(b"DIV"), LocalNameHash::of(b"div"));
assert_eq!(LocalNameHash::of(b"ScRiPt"), LocalNameHash::of(b"script"));
assert_eq!(
LocalNameHash::of(b"script"),
LocalNameHash::from_static(b"script")
);
assert!(LocalNameHash::of(b"").is_none());
assert!(!LocalNameHash::of(b"div").is_none());
}
#[test]
fn names_are_collision_free() {
let mut seen: HashMap<u64, Vec<u8>> = HashMap::default();
let mut check = |name: &[u8]| {
let hash = LocalNameHash::of(name);
if let Some(prev) = seen.insert(hash.0, name.to_vec())
&& !prev.eq_ignore_ascii_case(name)
{
panic!("collision: {prev:?} vs {name:?}");
}
};
for a in b'a'..=b'z' {
check(&[a]);
for b in b'a'..=b'z' {
check(&[a, b]);
for c in b'a'..=b'z' {
check(&[a, b, c]);
}
}
}
for tag in [
b"blockquote".as_slice(),
b"figcaption",
b"foreignobject",
b"plaintext",
b"textarea",
b"template",
b"noscript",
b"optgroup",
] {
check(tag);
}
}
}