use crate::parser::value::scan::{comment_end, is_comment_start};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueSeparator {
Comma,
Whitespace,
None,
}
pub(crate) fn classify_separators(s: &str) -> ValueSeparator {
let mut in_parens: u32 = 0;
let mut in_quote = false;
let mut quote_char = 0u8;
let mut whitespace_seen = false;
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if !in_quote && is_comment_start(bytes, i) {
if in_parens == 0 {
whitespace_seen = true;
}
i = comment_end(bytes, i);
continue;
}
if b == b'\\'
&& let Some(len) = crate::escapes::escape_len(s, i)
{
i += len;
continue;
}
match b {
b'\'' | b'"' if !in_quote => {
in_quote = true;
quote_char = b;
}
_ if in_quote && b == quote_char => in_quote = false,
b'(' if !in_quote => in_parens += 1,
b')' if !in_quote => in_parens = in_parens.saturating_sub(1),
b',' if in_parens == 0 && !in_quote => return ValueSeparator::Comma,
_ if in_parens == 0 && !in_quote && b.is_ascii_whitespace() => whitespace_seen = true,
_ => {}
}
i += 1;
}
if whitespace_seen {
ValueSeparator::Whitespace
} else {
ValueSeparator::None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_value_has_no_separators() {
assert_eq!(classify_separators("auto"), ValueSeparator::None);
assert_eq!(classify_separators("10px"), ValueSeparator::None);
assert_eq!(classify_separators("#fff"), ValueSeparator::None);
}
#[test]
fn comma_is_detected() {
assert_eq!(classify_separators("red, blue"), ValueSeparator::Comma);
assert_eq!(classify_separators("a,b,c"), ValueSeparator::Comma);
}
#[test]
fn whitespace_is_detected() {
assert_eq!(
classify_separators("1px solid red"),
ValueSeparator::Whitespace
);
assert_eq!(classify_separators("a\tb"), ValueSeparator::Whitespace);
}
#[test]
fn comma_takes_priority_over_earlier_whitespace() {
assert_eq!(
classify_separators("red blue, green"),
ValueSeparator::Comma
);
}
#[test]
fn separators_inside_parens_are_ignored() {
assert_eq!(classify_separators("rgba(1, 2, 3)"), ValueSeparator::None);
assert_eq!(classify_separators("calc(1px + 2px)"), ValueSeparator::None);
assert_eq!(
classify_separators("rgba(1, 2, 3), blue"),
ValueSeparator::Comma
);
}
#[test]
fn separators_inside_quotes_are_ignored() {
assert_eq!(classify_separators(r#""foo, bar""#), ValueSeparator::None);
assert_eq!(classify_separators(r#""foo bar""#), ValueSeparator::None);
assert_eq!(
classify_separators(r#""foo, bar", baz"#),
ValueSeparator::Comma
);
}
#[test]
fn separators_inside_comments_are_ignored() {
assert_eq!(
classify_separators("red/*,*/blue"),
ValueSeparator::Whitespace
);
assert_eq!(
classify_separators("red/* x */blue"),
ValueSeparator::Whitespace
);
}
#[test]
fn a_top_level_comment_is_a_separator_in_every_position() {
for s in ["/* c */a", "a/* c */b", "a/* c */", "0.3s/* c */5px"] {
assert_eq!(
classify_separators(s),
ValueSeparator::Whitespace,
"top-level comment boundary in {s:?}"
);
}
assert_eq!(classify_separators("f(a/* c */b)"), ValueSeparator::None);
assert_eq!(classify_separators("a/* c */b, c"), ValueSeparator::Comma);
}
#[test]
fn whitespace_outside_comment_still_counts() {
assert_eq!(
classify_separators("/* x */ red"),
ValueSeparator::Whitespace
);
}
#[test]
fn multibyte_token_is_not_a_separator() {
assert_eq!(classify_separators("丠"), ValueSeparator::None);
assert_eq!(classify_separators("丠 中"), ValueSeparator::Whitespace);
assert_eq!(classify_separators("丠, 中"), ValueSeparator::Comma);
assert_eq!(classify_separators("calc(丠)"), ValueSeparator::None);
}
}