use super::CssParser;
use crate::comments::{comment_end_checked, is_comment_start};
use crate::lexer::{
IDENT_CONTINUE_LUT, Lexer, TokenKind, is_ascii_css_whitespace, string_end, url_arg_is_quoted,
url_token_close,
};
use crate::parser::value::lists::ValueSeparator;
use crate::parser::value::scan::is_value_separator;
use tsv_lang::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum TerminatorKind {
Semicolon,
RightBrace,
Eof,
}
impl TerminatorKind {
pub(super) fn token(self) -> (TokenKind, usize) {
match self {
Self::Semicolon => (TokenKind::Semicolon, 1),
Self::RightBrace => (TokenKind::RightBrace, 1),
Self::Eof => (TokenKind::Eof, 0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ValueFacts {
pub(super) terminator: usize,
pub(super) terminator_kind: TerminatorKind,
pub(super) value_end: usize,
pub(super) important_end: Option<usize>,
pub(super) has_comment: bool,
pub(super) is_empty: bool,
pub(super) group_closes_value: bool,
}
const SKIP: [bool; 256] = {
let mut t = [false; 256];
let mut i = 0;
while i < 128 {
let b = i as u8;
t[i] = !value_scan_inspects(b) && (is_ascii_css_whitespace(b) || is_inert_content(b));
i += 1;
}
t
};
const SKIP_CHUNK: usize = 8;
#[inline]
fn skip_run_end(bytes: &[u8], from: usize) -> usize {
let mut i = from;
while let Some(chunk) = bytes.get(i..i + SKIP_CHUNK) {
if let Some(k) = chunk.iter().position(|&b| !SKIP[b as usize]) {
return i + k;
}
i += SKIP_CHUNK;
}
while i < bytes.len() && SKIP[bytes[i] as usize] {
i += 1;
}
i
}
const fn value_scan_inspects(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'{' | b'}' | b'[' | b']' | b';' | b'"' | b'\'' | b'/' | b'!' | b'\\'
) || is_value_separator(b)
}
const fn is_inert_content(b: u8) -> bool {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'_' | b'-'
| b'.'
| b'#'
| b'%'
| b'*'
| b'&'
| b'@'
| b'='
| b'^'
| b'?'
| b'~'
| b'>'
| b'<'
| b'+'
| b':'
| b','
| b'|'
| b'$'
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct DeclarationHead {
pub(super) colon: usize,
pub(super) gap_comment: bool,
pub(super) value_start: usize,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ScannedDeclaration {
pub(super) head: DeclarationHead,
pub(super) facts: ValueFacts,
pub(super) class: Option<ValueSeparator>,
}
enum Disambiguation {
Rule,
Declaration(ScannedDeclaration),
}
pub(super) fn scan_declaration_head_bytes(bytes: &[u8], from: usize) -> Option<DeclarationHead> {
let (colon, gap_comment) = colon_after_trivia(bytes, from)?;
head_after_colon(bytes, colon, gap_comment)
}
#[inline]
fn head_after_colon(bytes: &[u8], colon: usize, gap_comment: bool) -> Option<DeclarationHead> {
let len = bytes.len();
let mut i = colon + 1;
while i < len && is_ascii_css_whitespace(bytes[i]) {
i += 1;
}
if bytes.get(i).is_some_and(|b| !b.is_ascii()) {
return None;
}
Some(DeclarationHead {
colon,
gap_comment,
value_start: i,
})
}
fn scan_rule_or_declaration_and_value_bytes(
source: &str,
head: DeclarationHead,
) -> Option<Disambiguation> {
match scan_value_core::<true>(source, head.value_start)? {
ValueScanOutcome::Rule => Some(Disambiguation::Rule),
ValueScanOutcome::Value(facts, class) => {
Some(Disambiguation::Declaration(ScannedDeclaration {
head,
facts,
class,
}))
}
}
}
fn scan_rule_or_declaration(
parser: &CssParser<'_, '_>,
from: usize,
colon: usize,
gap_comment: bool,
) -> Result<bool, Error> {
let source = parser.source();
let head = head_after_colon(source.as_bytes(), colon, gap_comment);
match head.and_then(|head| scan_rule_or_declaration_and_value_bytes(source, head)) {
Some(outcome) => {
let is_rule = matches!(outcome, Disambiguation::Rule);
#[cfg(debug_assertions)]
{
let expected = scan_rule_or_declaration_tokens(source, parser.base_offset(), from);
assert!(
expected.as_ref().is_ok_and(|expected| *expected == is_rule),
"rule-or-declaration byte scan disagreed with the token walk at {from}: \
scan said {is_rule}, walk said {expected:?}"
);
if let Disambiguation::Declaration(scanned) = &outcome {
let value_start = scanned.head.value_start;
let expected_facts =
scan_value_tokens(source, parser.base_offset(), value_start);
assert!(
expected_facts
.as_ref()
.is_ok_and(|expected| *expected == scanned.facts),
"fused value scan disagreed with the token walk at {value_start}: \
scan said {:?}, walk said {expected_facts:?}",
scanned.facts
);
}
}
parser.stash_declaration(match outcome {
Disambiguation::Declaration(scanned) => Some((from, scanned)),
Disambiguation::Rule => None,
});
Ok(is_rule)
}
None => rule_or_declaration_by_tokens(parser, from),
}
}
fn rule_or_declaration_by_tokens(parser: &CssParser<'_, '_>, from: usize) -> Result<bool, Error> {
parser.stash_declaration(None);
scan_rule_or_declaration_tokens(parser.source(), parser.base_offset(), from)
}
fn scan_rule_or_declaration_tokens(
source: &str,
base_offset: usize,
from: usize,
) -> Result<bool, Error> {
let mut lexer = Lexer::at_offset(&source[from..], base_offset + from);
let mut paren: u32 = 0;
loop {
let token = lexer.next_token()?;
match token.kind {
TokenKind::LeftParen => paren += 1,
TokenKind::RightParen => paren = paren.saturating_sub(1),
TokenKind::LeftBrace if paren == 0 => return Ok(true),
TokenKind::Semicolon | TokenKind::RightBrace if paren == 0 => return Ok(false),
TokenKind::Eof => return Ok(false),
_ => {}
}
}
}
pub(super) fn identifier_child_is_rule(parser: &CssParser<'_, '_>) -> Result<bool, Error> {
let bytes = parser.source().as_bytes();
let from = parser.current_end();
match colon_after_trivia(bytes, from) {
Some((colon, gap_comment)) => {
#[cfg(debug_assertions)]
{
let expected = parser.peek_past_boundary_whitespace();
assert!(
expected
.as_ref()
.is_ok_and(|expected| *expected == TokenKind::Colon),
"propertyโcolon byte walk disagreed with the token lookahead at {from}: \
walk found a `:`, lookahead said {expected:?}"
);
}
scan_rule_or_declaration(parser, from, colon, gap_comment)
}
None => match parser.peek_past_boundary_whitespace()? {
TokenKind::Colon => rule_or_declaration_by_tokens(parser, from),
_ => Ok(true),
},
}
}
#[inline]
fn colon_after_trivia(bytes: &[u8], from: usize) -> Option<(usize, bool)> {
let len = bytes.len();
let mut i = from;
let mut gap_comment = false;
loop {
while i < len && is_ascii_css_whitespace(bytes[i]) {
i += 1;
}
match bytes.get(i)? {
b':' => return Some((i, gap_comment)),
b'/' if is_comment_start(bytes, i) => {
gap_comment = true;
i = comment_end_checked(bytes, i)?;
}
_ => return None,
}
}
}
pub(super) fn scan_value(
parser: &CssParser<'_, '_>,
value_start: usize,
) -> Result<(ValueFacts, Option<ValueSeparator>), Error> {
let source = parser.source();
match scan_value_bytes(source, value_start) {
Some((facts, class)) => {
#[cfg(debug_assertions)]
{
let expected = scan_value_tokens(source, parser.base_offset(), value_start);
assert!(
expected.as_ref().is_ok_and(|expected| *expected == facts),
"declaration value byte scan disagreed with the token walk at \
{value_start}: scan said {facts:?}, walk said {expected:?}"
);
}
Ok((facts, class))
}
None => scan_value_tokens(source, parser.base_offset(), value_start).map(|f| (f, None)),
}
}
fn scan_value_bytes(
source: &str,
value_start: usize,
) -> Option<(ValueFacts, Option<ValueSeparator>)> {
match scan_value_core::<false>(source, value_start) {
Some(ValueScanOutcome::Value(facts, class)) => Some((facts, class)),
_ => None,
}
}
#[cfg_attr(test, derive(Debug, PartialEq))]
enum ValueScanOutcome {
Rule,
Value(ValueFacts, Option<ValueSeparator>),
}
fn scan_value_core<const WANT_VERDICT: bool>(
source: &str,
value_start: usize,
) -> Option<ValueScanOutcome> {
let bytes = source.as_bytes();
let len = bytes.len();
let mut i = value_start;
i = skip_run_end(bytes, i);
if i > value_start && !is_ascii_css_whitespace(bytes[i - 1]) {
let terminator_kind = match bytes.get(i) {
None => Some(TerminatorKind::Eof),
Some(b';') => Some(TerminatorKind::Semicolon),
Some(b'}') => Some(TerminatorKind::RightBrace),
Some(_) => None,
};
if let Some(terminator_kind) = terminator_kind {
return Some(ValueScanOutcome::Value(
ValueFacts {
terminator: i,
terminator_kind,
value_end: i,
important_end: None,
has_comment: false,
is_empty: false,
group_closes_value: false,
},
Some(ValueSeparator::None),
));
}
}
let mut paren: u32 = 0;
let mut brace: u32 = 0;
let mut bracket: u32 = 0;
let mut has_comment = false;
let mut top_comma = usize::MAX;
let mut top_ws = usize::MAX;
let mut class_unstated = false;
let mut last_bang: Option<usize> = None;
let mut verdict_is_decl = false;
let mut first_group_close = usize::MAX;
let (terminator, terminator_kind) = loop {
if i < len && SKIP[bytes[i] as usize] {
i = skip_run_end(bytes, i + 1);
}
if i >= len {
break (len, TerminatorKind::Eof);
}
let b = bytes[i];
if is_value_separator(b) {
if paren == 0 {
if b == b',' {
top_comma = top_comma.min(i);
} else {
top_ws = top_ws.min(i);
}
}
i += 1;
continue;
}
match b {
b';' | b'}' => {
if WANT_VERDICT && paren == 0 {
verdict_is_decl = true;
}
if paren == 0 && brace == 0 && bracket == 0 {
let kind = if b == b';' {
TerminatorKind::Semicolon
} else {
TerminatorKind::RightBrace
};
break (i, kind);
}
if b == b'}' {
brace = brace.saturating_sub(1);
}
i += 1;
}
b'{' => {
if WANT_VERDICT && !verdict_is_decl && paren == 0 {
return Some(ValueScanOutcome::Rule);
}
brace += 1;
i += 1;
}
b'[' => {
bracket += 1;
i += 1;
}
b']' => {
bracket = bracket.saturating_sub(1);
i += 1;
}
b')' => {
if paren == 1 {
first_group_close = first_group_close.min(i);
}
paren = paren.saturating_sub(1);
i += 1;
}
b'(' => match paren_open_kind(source, bytes, value_start, i) {
ParenOpen::UrlToken { end } => {
class_unstated = true;
i = end;
}
ParenOpen::Nesting => {
paren += 1;
i += 1;
}
ParenOpen::UnterminatedUrl => return None,
},
b'"' | b'\'' => i = string_end(bytes, i).ok()?,
b'/' if is_comment_start(bytes, i) => {
has_comment = true;
i = comment_end_checked(bytes, i)?;
}
b'/' => i += 1,
b'!' => {
last_bang = Some(i);
i += 1;
}
_ => return None,
}
};
let important = last_bang.and_then(|bang| {
let name_start = crate::comments::skip_trivia_forward(bytes, bang + 1, terminator);
let mut name_end = name_start;
while name_end < terminator && IDENT_CONTINUE_LUT[bytes[name_end] as usize] {
name_end += 1;
}
if !source[name_start..name_end].eq_ignore_ascii_case("important") {
return None;
}
if crate::comments::skip_trivia_forward(bytes, name_end, terminator) < terminator {
return None;
}
Some((bang, name_end))
});
let (span_end, important_end) = match important {
Some((bang, name_end)) => (bang, Some(name_end)),
None => (terminator, None),
};
let value_end = trim_end(bytes, value_start, span_end);
let is_empty = crate::comments::skip_trivia_forward(bytes, value_start, span_end) >= span_end;
let class = if has_comment || class_unstated {
None
} else if top_comma < value_end {
Some(ValueSeparator::Comma)
} else if top_ws < value_end {
Some(ValueSeparator::Whitespace)
} else {
Some(ValueSeparator::None)
};
Some(ValueScanOutcome::Value(
ValueFacts {
terminator,
terminator_kind,
value_end,
important_end,
has_comment,
is_empty,
group_closes_value: first_group_close.wrapping_add(1) == value_end,
},
class,
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParenOpen {
Nesting,
UrlToken { end: usize },
UnterminatedUrl,
}
fn paren_open_kind(source: &str, bytes: &[u8], value_start: usize, open: usize) -> ParenOpen {
if open < 3 || open - 3 < value_start {
return ParenOpen::Nesting;
}
if !bytes[open - 3..open].eq_ignore_ascii_case(b"url") {
return ParenOpen::Nesting;
}
if open - 3 > value_start {
let prev = bytes[open - 4];
if IDENT_CONTINUE_LUT[prev as usize] || prev == b'$' {
return ParenOpen::Nesting;
}
}
if url_arg_is_quoted(source, open + 1) {
return ParenOpen::Nesting;
}
match url_token_close(bytes, open + 1) {
Some(end) => ParenOpen::UrlToken { end },
None => ParenOpen::UnterminatedUrl,
}
}
fn trim_end(bytes: &[u8], from: usize, to: usize) -> usize {
let mut e = to;
while e > from && is_ascii_css_whitespace(bytes[e - 1]) {
e -= 1;
}
e
}
fn scan_value_tokens(
source: &str,
base_offset: usize,
value_start: usize,
) -> Result<ValueFacts, Error> {
let mut lexer = Lexer::at_offset(source, base_offset);
lexer.seek(value_start);
let mut has_comment = false;
let mut value_end = value_start;
let mut part_count: usize = 0;
let mut last_is_bang = false;
let mut last_is_important = false;
let mut last_ends: (usize, usize) = (0, 0);
let mut prev_is_bang = false;
let mut prev_ends: (usize, usize) = (0, 0);
let mut paren: u32 = 0;
let mut brace: u32 = 0;
let mut bracket: u32 = 0;
let mut first_group_close = usize::MAX;
let (terminator, terminator_kind) = loop {
let token = lexer.next_token()?;
let decoded = lexer.decoded_str();
if token.kind == TokenKind::Eof {
break (token.start as usize, TerminatorKind::Eof);
}
if paren == 0 && brace == 0 && bracket == 0 {
match token.kind {
TokenKind::Semicolon => break (token.start as usize, TerminatorKind::Semicolon),
TokenKind::RightBrace => break (token.start as usize, TerminatorKind::RightBrace),
_ => {}
}
}
match token.kind {
TokenKind::LeftParen => paren += 1,
TokenKind::RightParen => {
if paren == 1 {
first_group_close = first_group_close.min(token.start as usize);
}
paren = paren.saturating_sub(1);
}
TokenKind::LeftBrace => brace += 1,
TokenKind::RightBrace => brace = brace.saturating_sub(1),
TokenKind::LeftBracket => bracket += 1,
TokenKind::RightBracket => bracket = bracket.saturating_sub(1),
_ => {}
}
let (start, end) = (token.start as usize, token.end as usize);
let (is_bang, is_important) = match token.kind {
TokenKind::Identifier => {
let text = decoded.unwrap_or_else(|| &source[start..end]);
(false, text.eq_ignore_ascii_case("important"))
}
TokenKind::String { .. }
| TokenKind::Number
| TokenKind::Percentage
| TokenKind::Dimension { .. } => (false, false),
TokenKind::Whitespace => continue,
TokenKind::Comment => {
has_comment = true;
value_end = end;
continue;
}
TokenKind::Bang => (true, false),
_ => {
let text = &source[start..end];
(text == "!", text.eq_ignore_ascii_case("important"))
}
};
prev_is_bang = last_is_bang;
prev_ends = last_ends;
last_is_bang = is_bang;
last_is_important = is_important;
last_ends = (value_end, end);
part_count += 1;
value_end = end;
};
let important_matched = part_count >= 2 && prev_is_bang && last_is_important;
let important_end = if important_matched {
let end_with_important = last_ends.1;
value_end = prev_ends.0;
Some(end_with_important)
} else {
None
};
let is_empty = if important_matched {
part_count - 2 == 0
} else {
part_count == 0
};
Ok(ValueFacts {
terminator,
terminator_kind,
value_end,
important_end,
has_comment,
is_empty,
group_closes_value: first_group_close.wrapping_add(1) == value_end,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skip_runs_end_where_the_table_ends() {
let skipped: Vec<u8> = (0..=255u8).filter(|&b| SKIP[usize::from(b)]).collect();
let reference = |bytes: &[u8], mut i: usize| {
while i < bytes.len() && SKIP[usize::from(bytes[i])] {
i += 1;
}
i
};
let mut graded = 0;
for len in 0..=3 * SKIP_CHUNK {
for (k, &c) in skipped.iter().enumerate() {
let mut run = vec![b'a'; len];
if len > 0 {
run[k % len] = c;
}
for stop in (0..=255u8).map(Some).chain([None]) {
for tail in [0, 1, SKIP_CHUNK - 1, SKIP_CHUNK] {
for from in [0, 3] {
let mut bytes = vec![b';'; from];
bytes.extend_from_slice(&run);
if let Some(stop) = stop {
bytes.push(stop);
bytes.extend(core::iter::repeat_n(b'a', tail));
}
assert_eq!(
skip_run_end(&bytes, from),
reference(&bytes, from),
"{bytes:?} from {from}"
);
graded += 1;
}
}
}
}
}
assert!(graded > 100_000, "{graded}");
}
fn scan_value_core_model<const WANT_VERDICT: bool>(
source: &str,
value_start: usize,
) -> Option<ValueScanOutcome> {
let bytes = source.as_bytes();
let len = bytes.len();
let mut i = value_start;
i = skip_run_end(bytes, i);
if i > value_start && !is_ascii_css_whitespace(bytes[i - 1]) {
let terminator_kind = match bytes.get(i) {
None => Some(TerminatorKind::Eof),
Some(b';') => Some(TerminatorKind::Semicolon),
Some(b'}') => Some(TerminatorKind::RightBrace),
Some(_) => None,
};
if let Some(terminator_kind) = terminator_kind {
return Some(ValueScanOutcome::Value(
ValueFacts {
terminator: i,
terminator_kind,
value_end: i,
important_end: None,
has_comment: false,
is_empty: false,
group_closes_value: false,
},
Some(ValueSeparator::None),
));
}
}
let mut paren: u32 = 0;
let mut brace: u32 = 0;
let mut bracket: u32 = 0;
let mut has_comment = false;
let mut top_comma = usize::MAX;
let mut top_ws = usize::MAX;
let mut class_unstated = false;
let mut last_bang: Option<usize> = None;
let mut verdict_is_decl = false;
let mut first_group_close = usize::MAX;
let (terminator, terminator_kind) = loop {
if i < len && SKIP[bytes[i] as usize] {
i = skip_run_end(bytes, i + 1);
}
if i >= len {
break (len, TerminatorKind::Eof);
}
let b = bytes[i];
if is_value_separator(b) {
if paren == 0 {
if b == b',' {
top_comma = top_comma.min(i);
} else {
top_ws = top_ws.min(i);
}
}
i += 1;
continue;
}
let at_top = paren == 0 && brace == 0 && bracket == 0;
if WANT_VERDICT && !verdict_is_decl && paren == 0 {
match b {
b'{' => return Some(ValueScanOutcome::Rule),
b';' | b'}' => verdict_is_decl = true,
_ => {}
}
}
match b {
b';' if at_top => break (i, TerminatorKind::Semicolon),
b'}' if at_top => break (i, TerminatorKind::RightBrace),
b';' => i += 1,
b'}' => {
brace = brace.saturating_sub(1);
i += 1;
}
b'{' => {
brace += 1;
i += 1;
}
b'[' => {
bracket += 1;
i += 1;
}
b']' => {
bracket = bracket.saturating_sub(1);
i += 1;
}
b')' => {
if paren == 1 {
first_group_close = first_group_close.min(i);
}
paren = paren.saturating_sub(1);
i += 1;
}
b'(' => match paren_open_kind(source, bytes, value_start, i) {
ParenOpen::UrlToken { end } => {
class_unstated = true;
i = end;
}
ParenOpen::Nesting => {
paren += 1;
i += 1;
}
ParenOpen::UnterminatedUrl => return None,
},
b'"' | b'\'' => i = string_end(bytes, i).ok()?,
b'/' if is_comment_start(bytes, i) => {
has_comment = true;
i = comment_end_checked(bytes, i)?;
}
b'/' => i += 1,
b'!' => {
last_bang = Some(i);
i += 1;
}
_ => return None,
}
};
let important = last_bang.and_then(|bang| {
let name_start = crate::comments::skip_trivia_forward(bytes, bang + 1, terminator);
let mut name_end = name_start;
while name_end < terminator && IDENT_CONTINUE_LUT[bytes[name_end] as usize] {
name_end += 1;
}
if !source[name_start..name_end].eq_ignore_ascii_case("important") {
return None;
}
if crate::comments::skip_trivia_forward(bytes, name_end, terminator) < terminator {
return None;
}
Some((bang, name_end))
});
let (span_end, important_end) = match important {
Some((bang, name_end)) => (bang, Some(name_end)),
None => (terminator, None),
};
let value_end = trim_end(bytes, value_start, span_end);
let is_empty =
crate::comments::skip_trivia_forward(bytes, value_start, span_end) >= span_end;
let class = if has_comment || class_unstated {
None
} else if top_comma < value_end {
Some(ValueSeparator::Comma)
} else if top_ws < value_end {
Some(ValueSeparator::Whitespace)
} else {
Some(ValueSeparator::None)
};
Some(ValueScanOutcome::Value(
ValueFacts {
terminator,
terminator_kind,
value_end,
important_end,
has_comment,
is_empty,
group_closes_value: first_group_close.wrapping_add(1) == value_end,
},
class,
))
}
#[test]
fn single_dispatch_matches_the_split_arm_scan() {
const ALPHABET: [&str; 22] = [
"(",
")",
"[",
"]",
"{",
"}",
";",
",",
" ",
"\"",
"'",
"/",
"*",
"\\",
"!",
"a",
"5",
":",
"\n",
"url(",
"important",
"--x",
];
fn grade(source: &str) {
assert_eq!(
scan_value_core::<false>(source, 0),
scan_value_core_model::<false>(source, 0),
"plain scan, {source:?}"
);
assert_eq!(
scan_value_core::<true>(source, 0),
scan_value_core_model::<true>(source, 0),
"verdict scan, {source:?}"
);
}
let mut source = String::new();
let mut graded = 0u32;
for len in 0..=4u32 {
for case in 0..ALPHABET.len().pow(len) {
source.clear();
let mut rest = case;
for _ in 0..len {
source.push_str(ALPHABET[rest % ALPHABET.len()]);
rest /= ALPHABET.len();
}
grade(&source);
graded += 1;
}
}
let mut state = 0x9e37_79b9_7f4a_7c15u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..200_000 {
source.clear();
let len = 5 + next() % 12;
for _ in 0..len {
source.push_str(ALPHABET[(next() % ALPHABET.len() as u64) as usize]);
}
grade(&source);
graded += 1;
}
assert!(graded > 400_000, "{graded}");
}
#[test]
fn group_close_agrees_with_the_function_paren_scan() {
use crate::parser::value::plain_function_paren;
use crate::parser::value::scan::matching_close_paren;
const ALPHABET: [&str; 21] = [
"(",
")",
"[",
"]",
"{",
"}",
";",
",",
" ",
"\"",
"'",
"/",
"*",
"\\",
"!",
"a",
"-",
"5",
"url(",
"f(",
"important",
];
const HEAD: &str = "a{b:";
let mut arena = bumpalo::Bump::new();
let mut spent = 0u32;
let mut grade = |value: &str| {
let source = format!("{HEAD}{value}");
if let Some((facts, Some(ValueSeparator::None))) = scan_value_bytes(&source, HEAD.len())
{
let text = &source[HEAD.len()..facts.value_end];
if let Some(open) = plain_function_paren(text.as_bytes()) {
let closes = matching_close_paren(text, open) == Some(text.len() - 1);
assert_eq!(facts.group_closes_value, closes, "for {source:?}");
spent += u32::from(closes);
}
}
arena.reset();
let _ = crate::__private::parse(&source, &arena);
};
let mut value = String::new();
for len in 0..=4u32 {
for case in 0..ALPHABET.len().pow(len) {
value.clear();
let mut rest = case;
for _ in 0..len {
value.push_str(ALPHABET[rest % ALPHABET.len()]);
rest /= ALPHABET.len();
}
grade(&value);
}
}
let mut state = 0x9e37_79b9_7f4a_7c15u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..100_000 {
value.clear();
let len = 5 + next() % 8;
for _ in 0..len {
value.push_str(ALPHABET[(next() % ALPHABET.len() as u64) as usize]);
}
grade(&value);
}
assert!(spent > 500, "{spent}");
}
fn class_of(source: &str) -> Result<Option<ValueSeparator>, ()> {
let bytes = source.as_bytes();
let colon = source.find(':').expect("test source needs a `:`");
let mut value_start = colon + 1;
while is_ascii_css_whitespace(bytes[value_start]) {
value_start += 1;
}
scan_value_bytes(source, value_start)
.map(|(_, class)| class)
.ok_or(())
}
#[test]
fn separators_past_the_value_are_not_the_values_own() {
assert_eq!(
class_of("a{color: red !important;}"),
Ok(Some(ValueSeparator::None))
);
assert_eq!(class_of("a{color: red ;}"), Ok(Some(ValueSeparator::None)));
assert_eq!(class_of("a{color: red}"), Ok(Some(ValueSeparator::None)));
assert_eq!(
class_of("a{margin: 0 auto !important;}"),
Ok(Some(ValueSeparator::Whitespace))
);
assert_eq!(
class_of("a{font-family: a, b !important;}"),
Ok(Some(ValueSeparator::Comma))
);
}
#[test]
fn top_level_is_parens_and_quotes_only() {
assert_eq!(
class_of("a{width: calc(1px + 2px);}"),
Ok(Some(ValueSeparator::None))
);
assert_eq!(
class_of("a{transform: translate(1px, 2px);}"),
Ok(Some(ValueSeparator::None))
);
assert_eq!(
class_of("a{content: 'a b';}"),
Ok(Some(ValueSeparator::None))
);
assert_eq!(
class_of("a{content: 'a,b';}"),
Ok(Some(ValueSeparator::None))
);
assert_eq!(
class_of("a{--x: [a b];}"),
Ok(Some(ValueSeparator::Whitespace))
);
assert_eq!(
class_of("a{grid-template: 'a a' 1fr;}"),
Ok(Some(ValueSeparator::Whitespace))
);
assert_eq!(
class_of("a{font: a b, c;}"),
Ok(Some(ValueSeparator::Comma))
);
}
#[test]
fn opaque_interiors_withhold_the_class() {
assert_eq!(class_of("a{background: url(a.png);}"), Ok(None));
assert_eq!(class_of("a{color: /* c */ red;}"), Ok(None));
assert_eq!(class_of("a{color: red /* c */;}"), Ok(None));
assert_eq!(
class_of("a{background: url('a.png') no-repeat;}"),
Ok(Some(ValueSeparator::Whitespace))
);
}
#[test]
fn a_declining_scan_is_not_a_leaf_verdict() {
assert_eq!(class_of("a{content: a\\ b;}"), Err(()));
assert_eq!(class_of("a{font-family: \u{e9}x;}"), Err(()));
}
#[test]
fn a_single_run_value_agrees_with_the_token_walk() {
for source in [
"a{color:red;}",
"a{color:red}",
"a{color:red",
"a{width:1.5rem;}",
"a{--x:a\u{b};}",
"a{--x:a\u{b}}",
] {
let value_start = source.find(':').expect("test source needs a `:`") + 1;
let scanned = scan_value_bytes(source, value_start).map(|(facts, _)| facts);
let walked = scan_value_tokens(source, 0, value_start).ok();
assert_eq!(scanned, walked, "{source:?}");
}
assert_eq!(class_of("a{color:red;}"), Ok(Some(ValueSeparator::None)));
assert_eq!(class_of("a{--x:a\u{b};}"), Ok(Some(ValueSeparator::None)));
}
fn head_of(source: &str, property: &str) -> Option<(usize, bool, usize)> {
let from = source
.find(property)
.expect("test source holds the property")
+ property.len();
scan_declaration_head_bytes(source.as_bytes(), from)
.map(|head| (head.colon, head.gap_comment, head.value_start))
}
#[test]
fn declaration_head_locates_the_colon_and_the_value() {
assert_eq!(head_of("a{color: red}", "color"), Some((7, false, 9)));
assert_eq!(head_of("a{color:red}", "color"), Some((7, false, 8)));
assert_eq!(head_of("a{color\t:\n red}", "color"), Some((8, false, 11)));
assert_eq!(head_of("a{--x: 1}", "--x"), Some((5, false, 7)));
assert_eq!(head_of("a{color:/* c */red}", "color"), Some((7, false, 8)));
assert_eq!(head_of("a{color: ;}", "color"), Some((7, false, 9)));
assert_eq!(head_of("a{--x:", "--x"), Some((5, false, 6)));
}
#[test]
fn declaration_head_reports_a_gap_comment() {
assert_eq!(
head_of("a{color/* c */: red}", "color"),
Some((14, true, 16))
);
assert_eq!(
head_of("a{color /* c */ : red}", "color"),
Some((16, true, 18))
);
assert_eq!(
head_of("a{color /* a */ /* b */: red}", "color"),
Some((23, true, 25))
);
assert_eq!(head_of("a{--x/* c */: 1}", "--x"), Some((12, true, 14)));
}
#[test]
fn declaration_head_declines_what_the_token_head_reads_differently() {
assert_eq!(head_of("a{color \u{a0}: red}", "color"), None);
assert_eq!(head_of("a{color /* c */\u{a0}: red}", "color"), None);
assert_eq!(head_of("a{color: \u{85}red}", "color"), None);
assert_eq!(head_of("a{--x:\u{85}1}", "--x"), None);
assert_eq!(head_of("a{color /* c : red}", "color"), None);
assert_eq!(head_of("a{color red}", "color"), None);
assert_eq!(head_of("a{content: \u{e9}}", "content"), None);
}
fn identifier_child(source: &str) -> (bool, bool) {
let arena = bumpalo::Bump::new();
let parser = CssParser::new(source, 0, 0, &arena).expect("test source lexes");
assert_eq!(parser.current_kind(), TokenKind::Identifier);
let is_rule = identifier_child_is_rule(&parser).expect("test source disambiguates");
let stashed = parser.take_declaration(parser.current_end()).is_some();
(is_rule, stashed)
}
#[test]
fn identifier_child_walks_the_gap_once() {
assert_eq!(identifier_child("color: red;"), (false, true));
assert_eq!(identifier_child("color:red}"), (false, true));
assert_eq!(
identifier_child("color\t/* a */ /* b */\n: red"),
(false, true)
);
assert_eq!(identifier_child("filter:blur(5px);"), (false, true));
assert_eq!(identifier_child("span:hover { }"), (true, false));
assert_eq!(
identifier_child("span:not(:last-child)::after { }"),
(true, false)
);
assert_eq!(identifier_child("div { }"), (true, false));
assert_eq!(identifier_child("a.b { }"), (true, false));
assert_eq!(identifier_child("a /* c */ [b] { }"), (true, false));
assert_eq!(identifier_child("color \u{a0}: red;"), (false, false));
assert_eq!(
identifier_child("color /* c */\u{a0}: red;"),
(false, false)
);
assert_eq!(identifier_child("color: \u{85}red;"), (false, false));
assert_eq!(identifier_child("content: \u{e9};"), (false, false));
}
fn value_scans_agree_with_the_token_walk(source: &str) -> (bool, bool) {
let bytes = source.as_bytes();
let colon = source.find(':').expect("test source needs a `:`");
let mut value_start = colon + 1;
while value_start < bytes.len() && is_ascii_css_whitespace(bytes[value_start]) {
value_start += 1;
}
let walked = scan_value_tokens(source, 0, value_start).ok();
let plain = scan_value_bytes(source, value_start).map(|(facts, _)| facts);
if let Some(facts) = plain {
assert_eq!(
Some(facts),
walked,
"plain value scan disagreed: {source:?}"
);
}
let fused = scan_declaration_head_bytes(bytes, colon)
.and_then(|head| scan_rule_or_declaration_and_value_bytes(source, head));
match &fused {
Some(Disambiguation::Declaration(scanned)) => {
assert_eq!(
scan_rule_or_declaration_tokens(source, 0, colon).ok(),
Some(false),
"fused verdict disagreed: {source:?}"
);
assert_eq!(
Some(scanned.facts),
walked,
"fused value scan disagreed: {source:?}"
);
}
Some(Disambiguation::Rule) => panic!("a declaration read as a rule: {source:?}"),
None => {}
}
(plain.is_none(), fused.is_none())
}
#[test]
fn an_unterminated_url_token_declines_both_scans() {
for source in [
"a{color: url( \n",
"a{color: url( ",
"a{color: URL( \n",
"a{color: url(x \n",
"a{color: url(x",
"a{color: url(x;",
"a{color: x url( \n",
"a{--x: url( \n",
"a{b: f(url( \n",
"a{b: f(url(x \n",
"a{color: url(x\\)",
"a{color: url(x\\) ",
"a{color: url(x\\)\n",
] {
assert_eq!(
value_scans_agree_with_the_token_walk(source),
(true, true),
"{source:?}"
);
}
}
#[test]
fn paren_open_kind_tells_the_three_apart() {
let kind = |source: &str| {
let value_start = source.find(':').expect("test source needs a `:`") + 2;
let open = source.rfind('(').expect("test source needs a `(`");
paren_open_kind(source, source.as_bytes(), value_start, open)
};
assert_eq!(kind("a{b: url(x)}"), ParenOpen::UrlToken { end: 11 });
assert_eq!(kind("a{b: url(x\\))}"), ParenOpen::UrlToken { end: 13 });
assert_eq!(kind("a{b: url(x"), ParenOpen::UnterminatedUrl);
assert_eq!(kind("a{b: url(x\\)"), ParenOpen::UnterminatedUrl);
assert_eq!(kind("a{b: f(x"), ParenOpen::Nesting);
assert_eq!(kind("a{b: url('x"), ParenOpen::Nesting);
assert_eq!(kind("a{b: 5url(x"), ParenOpen::Nesting);
assert_eq!(kind("a{b: blurl(x"), ParenOpen::Nesting);
}
#[test]
fn a_terminated_url_and_an_unterminated_plain_paren_are_answered() {
for source in [
"a{color: url(x)}",
"a{color: url(x) }",
"a{color: url( x )\n",
"a{b: f( \n",
"a{b: f(x \n",
] {
assert_eq!(
value_scans_agree_with_the_token_walk(source),
(false, false),
"{source:?}"
);
}
}
#[test]
fn an_unterminated_url_token_is_the_token_walks_parse_error() {
for source in [
"a{color: url( \n",
"a{color: url(x \n",
"a{--x: url( \n",
"a{color: url(x\\) \n",
"a{color: url(x;",
"a{color: x url( \n",
"a{b: f(url( \n",
"@media x{a{color: url( \n",
"\n\ta {\n\t\tcolor: url(x;\n\t}\n",
] {
let arena = bumpalo::Bump::new();
let err = crate::__private::parse(source, &arena).expect_err(source);
assert!(
err.to_string().contains("Expected '}'"),
"{source:?}: {err}"
);
}
let arena = bumpalo::Bump::new();
assert!(crate::__private::parse("a{color: url(x)}", &arena).is_ok());
}
}