use std::ops::Range;
pub(crate) fn quoted_end(source: &str, start: usize) -> usize {
let bytes = source.as_bytes();
let quote = bytes[start];
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == quote {
return index + 1;
} else {
index += 1;
}
}
bytes.len()
}
pub(crate) fn block_comment_end(source: &str, start: usize) -> usize {
source[start + 2..]
.find("*/")
.map_or(source.len(), |offset| start + offset + 4)
}
pub(crate) fn next_char_boundary(source: &str, index: usize) -> usize {
next_char_boundary_from_bytes(source.as_bytes(), index, source.len())
}
pub(crate) fn next_char_boundary_from_bytes(bytes: &[u8], index: usize, limit: usize) -> usize {
if index >= limit || bytes[index].is_ascii() {
return (index + 1).min(limit);
}
let width = if bytes[index] & 0b1111_1000 == 0b1111_0000 {
4
} else if bytes[index] & 0b1111_0000 == 0b1110_0000 {
3
} else {
2
};
(index + width).min(limit)
}
pub(crate) fn ident_end(bytes: &[u8], mut index: usize, limit: usize) -> usize {
while index < limit {
if is_ident_byte(bytes[index]) {
index += 1;
} else if bytes[index] == b'\\' {
index = css_escape_end(bytes, index, limit);
} else {
break;
}
}
index
}
pub(crate) fn is_ident_start_byte(byte: u8) -> bool {
byte.is_ascii_alphabetic() || matches!(byte, b'-' | b'_' | b'\\') || byte >= 0x80
}
pub(crate) fn is_ident_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') || byte >= 0x80
}
pub(crate) fn css_escape_end(bytes: &[u8], start: usize, limit: usize) -> usize {
let mut index = start + 1;
if index >= limit {
return index;
}
if bytes[index].is_ascii_hexdigit() {
let mut digits = 0usize;
while index < limit && digits < 6 && bytes[index].is_ascii_hexdigit() {
index += 1;
digits += 1;
}
if index < limit && bytes[index].is_ascii_whitespace() {
if bytes[index] == b'\r' && index + 1 < limit && bytes.get(index + 1) == Some(&b'\n') {
index += 1;
}
index += 1;
}
index
} else {
next_char_boundary_from_bytes(bytes, index, limit)
}
}
pub(crate) fn identifier_value(token: &str) -> String {
let quoted = matches!(token.as_bytes().first(), Some(b'"' | b'\''));
if !quoted && !token.contains('\\') {
return token.to_string();
}
let bytes = token.as_bytes();
let mut value = String::with_capacity(token.len().saturating_sub(2 * usize::from(quoted)));
let mut index = usize::from(quoted);
let end = token.len().saturating_sub(usize::from(quoted));
while index < end {
if bytes[index] != b'\\' {
let next = next_char_boundary(token, index).min(end);
value.push_str(&token[index..next]);
index = next;
continue;
}
index += 1;
if index >= end {
break;
}
if bytes[index].is_ascii_hexdigit() {
let mut codepoint = 0u32;
let mut digits = 0usize;
while index < end && digits < 6 && bytes[index].is_ascii_hexdigit() {
codepoint = codepoint * 16 + u32::from(hex_value(bytes[index]));
index += 1;
digits += 1;
}
if index < end && bytes[index].is_ascii_whitespace() {
if bytes[index] == b'\r' && index + 1 < end && bytes.get(index + 1) == Some(&b'\n')
{
index += 1;
}
index += 1;
}
value.push(if codepoint == 0 {
char::REPLACEMENT_CHARACTER
} else {
char::from_u32(codepoint).unwrap_or(char::REPLACEMENT_CHARACTER)
});
} else if matches!(bytes[index], b'\n' | b'\r' | b'\x0c') {
if bytes[index] == b'\r' && index + 1 < end && bytes.get(index + 1) == Some(&b'\n') {
index += 1;
}
index += 1;
} else {
let next = next_char_boundary(token, index).min(end);
value.push_str(&token[index..next]);
index = next;
}
}
value
}
pub(crate) fn css_identifier_eq(identifier: &str, expected: &str) -> bool {
if identifier.contains('\\') {
identifier_value(identifier).eq_ignore_ascii_case(expected)
} else {
identifier.eq_ignore_ascii_case(expected)
}
}
fn hex_value(byte: u8) -> u8 {
match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
b'A'..=b'F' => byte - b'A' + 10,
_ => 0,
}
}
pub(crate) struct PseudoName {
pub(crate) name: Range<usize>,
pub(crate) is_element: bool,
}
pub(crate) fn pseudo_name(source: &str, start: usize) -> Option<PseudoName> {
let bytes = source.as_bytes();
let is_element = bytes.get(start + 1) == Some(&b':');
let name_start = start + 1 + usize::from(is_element);
if !bytes
.get(name_start)
.is_some_and(|byte| is_ident_start_byte(*byte))
{
return None;
}
let name_end = ident_end(bytes, name_start, bytes.len());
Some(PseudoName {
name: name_start..name_end,
is_element,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn advances_past_strings_comments_and_escapes() {
assert_eq!(quoted_end("\"a\\\"b\" tail", 0), 6);
assert_eq!(quoted_end("'unterminated", 0), 13);
assert_eq!(block_comment_end("/* c */x", 0), 7);
assert_eq!(block_comment_end("/* unterminated", 0), 15);
assert_eq!(css_escape_end(b"\\26 x", 0, 5), 4);
assert_eq!(css_escape_end(b"\\@rest", 0, 6), 2);
}
#[test]
fn decodes_escaped_and_quoted_identifiers() {
assert_eq!(identifier_value("plain"), "plain");
assert_eq!(identifier_value("\"quoted\""), "quoted");
assert_eq!(identifier_value("\\68 ost"), "host");
assert!(css_identifier_eq("\\68 ost", "host"));
assert!(css_identifier_eq("HOST", "host"));
assert!(!css_identifier_eq("hosted", "host"));
}
#[test]
fn reads_pseudo_tokens_and_paren_groups() {
let pseudo = pseudo_name(":hover", 0).expect("pseudo-class");
assert!(!pseudo.is_element);
assert_eq!(&":hover"[pseudo.name], "hover");
let element = pseudo_name("::before", 0).expect("pseudo-element");
assert!(element.is_element);
assert_eq!(&"::before"[element.name], "before");
assert!(pseudo_name(":", 0).is_none());
}
}