mod impls;
use cssparser::{Parser as CssParser, ToCss, Token, match_ignore_ascii_case};
use selectors::parser::{
Component, NonTSPseudoClass, ParseRelative, PseudoElement, RelativeSelector, Selector,
SelectorImpl, SelectorList, SelectorParseErrorKind,
};
use selectors::visitor::{SelectorListKind, SelectorVisitor};
use std::fmt;
pub(crate) use impls::CssString;
use crate::translate::error::{Error, ParseErrorKind};
#[derive(Clone, Debug)]
pub(crate) struct CssToXpathImpl;
impl SelectorImpl for CssToXpathImpl {
type ExtraMatchingData<'a> = ();
type AttrValue = CssString;
type Identifier = CssString;
type LocalName = CssString;
type NamespaceUrl = CssString;
type NamespacePrefix = CssString;
type BorrowedNamespaceUrl = str;
type BorrowedLocalName = str;
type NonTSPseudoClass = PseudoClass;
type PseudoElement = NeverPseudoElement;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum PseudoClass {
AnyLink,
Link,
Visited,
Hover,
Active,
Focus,
FocusWithin,
FocusVisible,
Target,
TargetWithin,
LocalLink,
Enabled,
Disabled,
Checked,
Required,
Optional,
ReadOnly,
ReadWrite,
Default,
PlaceholderShown,
Lang(Vec<String>),
Dir(String),
}
impl PseudoClass {
fn name(&self) -> &'static str {
match self {
PseudoClass::AnyLink => "any-link",
PseudoClass::Link => "link",
PseudoClass::Visited => "visited",
PseudoClass::Hover => "hover",
PseudoClass::Active => "active",
PseudoClass::Focus => "focus",
PseudoClass::FocusWithin => "focus-within",
PseudoClass::FocusVisible => "focus-visible",
PseudoClass::Target => "target",
PseudoClass::TargetWithin => "target-within",
PseudoClass::LocalLink => "local-link",
PseudoClass::Enabled => "enabled",
PseudoClass::Disabled => "disabled",
PseudoClass::Checked => "checked",
PseudoClass::Required => "required",
PseudoClass::Optional => "optional",
PseudoClass::ReadOnly => "read-only",
PseudoClass::ReadWrite => "read-write",
PseudoClass::Default => "default",
PseudoClass::PlaceholderShown => "placeholder-shown",
PseudoClass::Lang(_) => "lang",
PseudoClass::Dir(_) => "dir",
}
}
}
impl ToCss for PseudoClass {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
dest.write_char(':')?;
dest.write_str(self.name())?;
match self {
PseudoClass::Lang(ranges) => {
dest.write_char('(')?;
for (i, range) in ranges.iter().enumerate() {
if i > 0 {
dest.write_str(", ")?;
}
if range.is_empty() {
dest.write_str("\"\"")?;
}
for (j, piece) in range.split('*').enumerate() {
if j > 0 {
dest.write_char('*')?;
}
if !piece.is_empty() {
cssparser::serialize_identifier(piece, dest)?;
}
}
}
dest.write_char(')')
}
PseudoClass::Dir(value) => {
dest.write_char('(')?;
cssparser::serialize_identifier(value, dest)?;
dest.write_char(')')
}
_ => Ok(()),
}
}
}
impl NonTSPseudoClass for PseudoClass {
fn is_active_or_hover(&self) -> bool {
matches!(self, PseudoClass::Active | PseudoClass::Hover)
}
fn is_user_action_state(&self) -> bool {
matches!(
self,
PseudoClass::Active
| PseudoClass::Hover
| PseudoClass::Focus
| PseudoClass::FocusWithin
| PseudoClass::FocusVisible
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum NeverPseudoElement {}
impl ToCss for NeverPseudoElement {
#[allow(clippy::uninhabited_references)]
fn to_css<W: fmt::Write>(&self, _dest: &mut W) -> fmt::Result {
match *self {}
}
}
impl PseudoElement for NeverPseudoElement {}
pub(crate) struct CssToXpathParser<'a> {
forgiving: bool,
default_namespace: &'a CssString,
probe_end: Option<usize>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Rejected {
Incomplete,
Invalid,
}
impl CssToXpathParser<'_> {
fn functional_error(
&self,
parser: &CssParser<'_>,
why: Rejected,
) -> cssparser::ParseError<SelectorParseErrorKind> {
if why == Rejected::Incomplete && self.probe_end == Some(parser.position().byte_index()) {
return cssparser::ParseError::from_basic_kind(
cssparser::BasicParseErrorKind::EndOfInput,
);
}
cssparser::ParseError::custom(SelectorParseErrorKind::UnsupportedPseudoClassOrElement)
}
}
impl<'i> selectors::parser::Parser<'i> for CssToXpathParser<'_> {
type Impl = CssToXpathImpl;
type Error = SelectorParseErrorKind;
fn allow_forgiving_selectors(&self) -> bool {
self.forgiving
}
fn parse_is_and_where(&self) -> bool {
true
}
fn is_is_alias(&self, name: &str) -> bool {
name.eq_ignore_ascii_case("matches")
}
fn parse_has(&self) -> bool {
true
}
fn parse_nth_child_of(&self) -> bool {
true
}
fn parse_non_ts_pseudo_class(
&self,
name: cssparser::CowRcStr<'i>,
) -> Result<PseudoClass, cssparser::ParseError<Self::Error>> {
let pc = match_ignore_ascii_case! { &name,
"any-link" => PseudoClass::AnyLink,
"link" => PseudoClass::Link,
"visited" => PseudoClass::Visited,
"hover" => PseudoClass::Hover,
"active" => PseudoClass::Active,
"focus" => PseudoClass::Focus,
"focus-within" => PseudoClass::FocusWithin,
"focus-visible" => PseudoClass::FocusVisible,
"target" => PseudoClass::Target,
"target-within" => PseudoClass::TargetWithin,
"local-link" => PseudoClass::LocalLink,
"enabled" => PseudoClass::Enabled,
"disabled" => PseudoClass::Disabled,
"checked" => PseudoClass::Checked,
"required" => PseudoClass::Required,
"optional" => PseudoClass::Optional,
"read-only" => PseudoClass::ReadOnly,
"read-write" => PseudoClass::ReadWrite,
"default" => PseudoClass::Default,
"placeholder-shown" => PseudoClass::PlaceholderShown,
_ => {
return Err(cssparser::ParseError::custom(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement,
));
},
};
Ok(pc)
}
fn parse_non_ts_functional_pseudo_class(
&self,
name: cssparser::CowRcStr<'i>,
parser: &mut CssParser<'i>,
_after_part: bool,
) -> Result<PseudoClass, cssparser::ParseError<Self::Error>> {
if name.eq_ignore_ascii_case("dir") {
let value = match parser.next() {
Ok(Token::Ident(v)) => v.as_ref().to_owned(),
Ok(_) => return Err(self.functional_error(parser, Rejected::Invalid)),
Err(_) => return Err(self.functional_error(parser, Rejected::Incomplete)),
};
if parser.next().is_ok() {
return Err(self.functional_error(parser, Rejected::Invalid));
}
return Ok(PseudoClass::Dir(value));
}
if !name.eq_ignore_ascii_case("lang") {
return Err(self.functional_error(parser, Rejected::Invalid));
}
parse_lang_ranges(parser)
.map(PseudoClass::Lang)
.map_err(|why| self.functional_error(parser, why))
}
fn namespace_for_prefix(&self, prefix: &CssString) -> Option<CssString> {
Some(prefix.clone())
}
fn default_namespace(&self) -> Option<CssString> {
Some(self.default_namespace.clone())
}
}
fn parse_lang_ranges(parser: &mut CssParser<'_>) -> Result<Vec<String>, Rejected> {
let mut ranges: Vec<String> = Vec::new();
let mut current = String::new();
let mut started = false;
let mut adjacent = true;
while let Ok(token) = parser.next_including_whitespace_and_comments().cloned() {
let piece = match token {
Token::WhiteSpace(_) | Token::Comment(_) => {
adjacent = false;
continue;
}
Token::Comma => {
if !started {
return Err(Rejected::Invalid);
}
ranges.push(std::mem::take(&mut current));
(started, adjacent) = (false, true);
continue;
}
Token::Ident(ref v) | Token::QuotedString(ref v) => v.as_ref().to_owned(),
Token::Delim('*') => "*".to_owned(),
_ => return Err(Rejected::Invalid),
};
if started && !adjacent {
return Err(Rejected::Invalid);
}
current.push_str(&piece);
started = true;
adjacent = true;
}
if !started {
return Err(Rejected::Incomplete);
}
ranges.push(current);
Ok(ranges)
}
pub const MAX_NESTING_DEPTH: usize = 32;
struct Scan {
column_combinator: Option<usize>,
too_deep: Option<usize>,
nesting_selector: Option<usize>,
misplaced_scope: Option<(usize, ScopeSite)>,
host: Option<usize>,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum ScopeSite {
Functional,
NotLeftmost,
}
impl ScopeSite {
fn construct(self) -> &'static str {
match self {
ScopeSite::Functional => "the `:scope` pseudo-class inside a functional pseudo-class",
ScopeSite::NotLeftmost => "the `:scope` pseudo-class outside the leftmost compound",
}
}
}
#[derive(Default)]
struct GroupPosition {
content_seen: bool,
space_pending: bool,
combinator_seen: bool,
}
fn scan(css: &str) -> Scan {
let bytes = css.as_bytes();
let mut i = 0;
let mut quote: Option<u8> = None;
let mut depth: usize = 0;
let mut brackets: usize = 0;
let mut group = GroupPosition::default();
let mut scan = Scan {
column_combinator: None,
too_deep: None,
nesting_selector: None,
misplaced_scope: None,
host: None,
};
while i < bytes.len() {
let b = bytes[i];
match quote {
Some(q) => {
if b == b'\\' {
i += 1; } else if b == q {
quote = None;
}
}
None => {
if depth == 0 && brackets == 0 {
match b {
b',' => group = GroupPosition::default(),
b'>' | b'+' | b'~' => {
group.combinator_seen = true;
group.content_seen = true;
group.space_pending = false;
}
b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => {
group.space_pending |= group.content_seen;
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {}
_ => {
group.combinator_seen |= group.space_pending;
group.space_pending = false;
group.content_seen = true;
}
}
}
match b {
b'\\' => i += 1, b'"' | b'\'' => quote = Some(b),
b'/' if bytes.get(i + 1) == Some(&b'*') => {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
i += 1;
}
b'|' if bytes.get(i + 1) == Some(&b'|') => {
scan.column_combinator.get_or_insert(i);
}
b'(' => {
depth += 1;
if depth > MAX_NESTING_DEPTH {
scan.too_deep.get_or_insert(i);
}
}
b')' => depth = depth.saturating_sub(1),
b'[' => brackets += 1,
b']' => brackets = brackets.saturating_sub(1),
b'&' => {
scan.nesting_selector.get_or_insert(i);
}
b':' if bytes[i..].starts_with(b":scope") && brackets == 0 => {
let site = if depth > 0 {
Some(ScopeSite::Functional)
} else if group.combinator_seen {
Some(ScopeSite::NotLeftmost)
} else {
None };
if let Some(site) = site {
scan.misplaced_scope.get_or_insert((i, site));
}
}
b':' if bytes[i..].starts_with(b":host(") => {
scan.host.get_or_insert(i);
}
_ => {}
}
}
}
i += 1;
}
scan
}
type ParseFailure = cssparser::ParseError<SelectorParseErrorKind>;
#[derive(Clone, Copy)]
struct Attempt<'a> {
forgiving: bool,
default_namespace: &'a CssString,
}
impl Attempt<'_> {
fn parse(self, css: &str) -> Result<SelectorList<CssToXpathImpl>, ParseFailure> {
self.run(css, None)
}
fn probe(self, css: &str) -> Result<(), ParseFailure> {
self.run(css, Some(css.len())).map(drop)
}
fn run(
self,
css: &str,
probe_end: Option<usize>,
) -> Result<SelectorList<CssToXpathImpl>, ParseFailure> {
SelectorList::parse(
&CssToXpathParser {
forgiving: self.forgiving,
default_namespace: self.default_namespace,
probe_end,
},
&mut CssParser::new(css),
ParseRelative::No,
)
}
}
pub(crate) fn parse(
css: &str,
default_namespace: Option<&str>,
) -> Result<SelectorList<CssToXpathImpl>, Error> {
let scan = scan(css);
if let Some(offset) = scan.column_combinator {
return Err(Error::unsupported_at("the `||` column combinator", offset));
}
if let Some(offset) = scan.too_deep {
return Err(Error::unsupported_at(
format!("functional pseudo-classes nested more than {MAX_NESTING_DEPTH} levels deep"),
offset,
));
}
if let Some(offset) = scan.nesting_selector {
return Err(Error::unsupported_at("the `&` nesting selector", offset));
}
let list = parse_lists(css, default_namespace)?;
if let Some((offset, site)) = scan.misplaced_scope {
return Err(Error::unsupported_at(site.construct(), offset));
}
if let Some(offset) = scan.host {
return Err(Error::unsupported_at("the `:host` pseudo-class", offset));
}
Ok(list)
}
fn parse_lists(
css: &str,
default_namespace: Option<&str>,
) -> Result<SelectorList<CssToXpathImpl>, Error> {
let default_namespace = CssString::from(default_namespace.unwrap_or(""));
let strict = Attempt {
forgiving: false,
default_namespace: &default_namespace,
};
let forgiving = Attempt {
forgiving: true,
..strict
};
let strict_error = match strict.parse(css) {
Ok(list) => return Ok(list),
Err(e) => e,
};
if !is_empty_selector(&strict_error) {
return Err(parse_error(css, strict, &strict_error));
}
match forgiving.parse(css) {
Ok(list) if dropped_nothing(&list) => Ok(list),
Ok(_) => Err(parse_error(css, strict, &strict_error)),
Err(e) if is_empty_selector(&strict_error) => Err(parse_error(css, forgiving, &e)),
Err(_) => Err(parse_error(css, strict, &strict_error)),
}
}
fn parse_error(css: &str, attempt: Attempt<'_>, e: &ParseFailure) -> Error {
let failure = locate(css, attempt, &e.kind);
let mut kind = ParseErrorKind::from_kind(&e.kind, failure.token.as_ref());
let offset = match kind {
ParseErrorKind::InvalidPosition | ParseErrorKind::Other(_) => failure.stopped,
_ => failure.at,
};
if matches!(kind, ParseErrorKind::EmptySelector)
&& let Some(blocked) = blocking_token(css, offset)
{
kind = blocked;
}
Error::Parse { kind, offset }
}
fn is_truncation_kind(kind: &cssparser::ParseErrorKind<SelectorParseErrorKind>) -> bool {
matches!(
kind,
cssparser::ParseErrorKind::Basic(cssparser::BasicParseErrorKind::EndOfInput)
| cssparser::ParseErrorKind::Custom(
SelectorParseErrorKind::EmptySelector | SelectorParseErrorKind::DanglingCombinator
)
)
}
struct Failure<'i> {
at: usize,
stopped: usize,
token: Option<Token<'i>>,
}
struct Spanned<'i> {
start: usize,
end: usize,
token: Token<'i>,
}
fn locate<'i>(
css: &'i str,
attempt: Attempt<'_>,
kind: &cssparser::ParseErrorKind<SelectorParseErrorKind>,
) -> Failure<'i> {
let tokens = tokens(css);
let ran_out = |at| Failure {
at,
stopped: at,
token: None,
};
if is_truncation_kind(kind) {
return ran_out(locate_truncation(css, attempt, &tokens, kind));
}
let Some(last) = tokens.len().checked_sub(1) else {
return ran_out(css.len());
};
let mut i = tokens[..last].partition_point(|t| match attempt.probe(&css[..t.end]) {
Ok(()) => true,
Err(e) => is_truncation_kind(&e.kind),
});
if i > 0 && matches!(tokens[i - 1].token, Token::BadString(_) | Token::BadUrl(_)) {
i -= 1;
}
if let cssparser::ParseErrorKind::Custom(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement,
) = kind
{
i = pseudo_name(&tokens, i);
}
let combinator = matches!(tokens[i].token, Token::Delim('>' | '+' | '~'));
let after_whitespace = tokens[..i]
.iter()
.rev()
.find(|t| !matches!(t.token, Token::Comment(_)))
.filter(|t| !combinator && matches!(t.token, Token::WhiteSpace(_)));
Failure {
at: tokens[i].start,
stopped: after_whitespace.map_or(tokens[i].end, |t| t.end),
token: Some(tokens[i].token.clone()),
}
}
fn locate_truncation(
css: &str,
attempt: Attempt<'_>,
tokens: &[Spanned<'_>],
kind: &cssparser::ParseErrorKind<SelectorParseErrorKind>,
) -> usize {
let ends: Vec<usize> = tokens
.iter()
.filter(|t| {
matches!(
t.token,
Token::Comma | Token::CloseParenthesis | Token::CloseSquareBracket
)
})
.map(|t| t.start)
.collect();
let end = ends
.get(ends.partition_point(|&end| attempt.probe(&css[..end]).is_ok()))
.copied()
.unwrap_or(css.len());
if matches!(kind, cssparser::ParseErrorKind::Basic(_)) {
return end;
}
let candidates: Vec<&Spanned<'_>> = tokens
.iter()
.take_while(|t| t.end <= end)
.filter(|t| !matches!(t.token, Token::WhiteSpace(_) | Token::Comment(_)))
.collect();
let unusable = candidates.partition_point(|t| {
let cut = &css[..t.end];
let awaits_compound = attempt.probe(cut).is_err_and(|e| {
matches!(
e.kind,
cssparser::ParseErrorKind::Custom(
SelectorParseErrorKind::EmptySelector
| SelectorParseErrorKind::DanglingCombinator
)
)
});
!(awaits_compound && attempt.probe(&format!("{cut}/**/*")).is_err())
});
candidates.get(unusable).map_or(end, |t| t.start)
}
fn pseudo_name(tokens: &[Spanned<'_>], i: usize) -> usize {
let after_colon = tokens[..i]
.iter()
.rev()
.find(|t| !matches!(t.token, Token::Comment(_)))
.is_some_and(|t| matches!(t.token, Token::Colon));
if after_colon && matches!(tokens[i].token, Token::Ident(_) | Token::Function(_)) {
return i;
}
let mut open = Vec::new();
for (j, t) in tokens[..i].iter().enumerate() {
match t.token {
Token::Function(_)
| Token::ParenthesisBlock
| Token::SquareBracketBlock
| Token::CurlyBracketBlock => open.push(j),
Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => {
open.pop();
}
_ => {}
}
}
open.into_iter()
.rev()
.find(|&j| matches!(tokens[j].token, Token::Function(_)))
.unwrap_or(i)
}
fn tokens(css: &str) -> Vec<Spanned<'_>> {
let mut tokens = Vec::new();
let mut base = 0;
loop {
let mut parser = CssParser::new(&css[base..]);
let restart = loop {
let start = base + parser.position().byte_index();
let Ok(token) = parser.next_including_whitespace_and_comments().cloned() else {
return tokens;
};
let end = base + parser.position().byte_index();
let opens_block = matches!(
token,
Token::Function(_)
| Token::ParenthesisBlock
| Token::SquareBracketBlock
| Token::CurlyBracketBlock
);
tokens.push(Spanned { start, end, token });
if opens_block {
break end;
}
};
base = restart;
}
}
fn blocking_token(css: &str, offset: usize) -> Option<ParseErrorKind> {
let mut parser = CssParser::new(css.get(offset..)?);
match parser.next() {
Ok(Token::Comma) | Err(_) => None,
Ok(token) => Some(ParseErrorKind::unexpected_token(token)),
}
}
fn is_empty_selector(e: &ParseFailure) -> bool {
matches!(
e.kind,
cssparser::ParseErrorKind::Custom(SelectorParseErrorKind::EmptySelector)
)
}
fn dropped_nothing(list: &SelectorList<CssToXpathImpl>) -> bool {
list.slice()
.iter()
.all(|selector| selector.visit(&mut DroppedArgument))
}
struct DroppedArgument;
impl SelectorVisitor for DroppedArgument {
type Impl = CssToXpathImpl;
fn visit_simple_selector(&mut self, component: &Component<CssToXpathImpl>) -> bool {
!matches!(component, Component::Invalid(_))
}
fn visit_selector_list(
&mut self,
_list_kind: SelectorListKind,
list: &[Selector<CssToXpathImpl>],
) -> bool {
if is_empty_forgiving_list(list) {
return true;
}
list.iter().all(|nested| nested.visit(self))
}
fn visit_relative_selector_list(&mut self, list: &[RelativeSelector<CssToXpathImpl>]) -> bool {
list.iter().all(|relative| relative.selector.visit(self))
}
}
pub(crate) fn is_empty_forgiving_list(list: &[Selector<CssToXpathImpl>]) -> bool {
let [selector] = list else {
return false;
};
let mut components = selector.iter_raw_match_order();
let Some(Component::Invalid(source)) = components.next() else {
return false;
};
if components.next().is_some() {
return false;
}
CssParser::new(source.as_str()).is_exhausted()
}
#[cfg(test)]
mod tests {
use super::*;
fn css(pc: &PseudoClass) -> String {
let mut s = String::new();
pc.to_css(&mut s).unwrap();
s
}
#[test]
fn pseudo_class_to_css_names() {
assert_eq!(css(&PseudoClass::AnyLink), ":any-link");
assert_eq!(css(&PseudoClass::Link), ":link");
assert_eq!(css(&PseudoClass::Visited), ":visited");
assert_eq!(css(&PseudoClass::Hover), ":hover");
assert_eq!(css(&PseudoClass::Active), ":active");
assert_eq!(css(&PseudoClass::Focus), ":focus");
assert_eq!(css(&PseudoClass::FocusWithin), ":focus-within");
assert_eq!(css(&PseudoClass::FocusVisible), ":focus-visible");
assert_eq!(css(&PseudoClass::Target), ":target");
assert_eq!(css(&PseudoClass::TargetWithin), ":target-within");
assert_eq!(css(&PseudoClass::LocalLink), ":local-link");
assert_eq!(css(&PseudoClass::Enabled), ":enabled");
assert_eq!(css(&PseudoClass::Disabled), ":disabled");
assert_eq!(css(&PseudoClass::Checked), ":checked");
assert_eq!(css(&PseudoClass::Required), ":required");
assert_eq!(css(&PseudoClass::Optional), ":optional");
}
#[test]
fn pseudo_class_to_css_lang() {
assert_eq!(css(&PseudoClass::Lang(vec!["en".into()])), ":lang(en)");
assert_eq!(
css(&PseudoClass::Lang(vec!["en".into(), "fr".into()])),
":lang(en, fr)"
);
assert_eq!(css(&PseudoClass::Lang(vec!["de-*".into()])), ":lang(de-*)");
assert_eq!(css(&PseudoClass::Lang(vec!["*".into()])), ":lang(*)");
assert_eq!(
css(&PseudoClass::Lang(vec!["de-*".into(), "*".into()])),
":lang(de-*, *)"
);
assert_eq!(css(&PseudoClass::Lang(vec!["1x".into()])), ":lang(\\31 x)");
assert_eq!(css(&PseudoClass::Lang(vec![String::new()])), ":lang(\"\")");
assert_eq!(
css(&PseudoClass::Lang(vec![String::new(), "en".into()])),
":lang(\"\", en)"
);
}
#[test]
fn lang_range_grammar() {
fn ranges(css: &str) -> Option<Vec<String>> {
let mut parser = CssParser::new(css);
parser.expect_function_matching("lang").ok()?;
parser
.parse_nested_block(|p| {
Ok::<_, cssparser::ParseError<()>>(parse_lang_ranges(p).ok())
})
.ok()?
}
let one = |css: &str, range: &str| {
assert_eq!(
ranges(css).as_deref(),
Some(&[range.to_owned()][..]),
"{css}"
);
};
one("lang(en)", "en");
one("lang( en )", "en");
one("lang(\"en\")", "en");
one("lang(en-*)", "en-*");
one("lang(*)", "*");
one("lang(*-CH)", "*-CH");
one("lang(\"en nz\")", "en nz");
one("lang(\"\")", "");
one("lang(en-)", "en-");
one("lang(--x)", "--x");
one("lang(en--)", "en--");
one("lang(en*)", "en*");
one("lang(*en)", "*en");
assert_eq!(
ranges("lang( en , fr )"),
Some(vec!["en".to_owned(), "fr".to_owned()])
);
for css in [
"lang()",
"lang(en fr)", "lang(en *)", "lang(,)",
"lang(,en)",
"lang(en,)",
"lang(en,,fr)",
"lang(5)",
"lang(-)",
"lang(en/**/fr)", ] {
assert_eq!(ranges(css), None, "{css}");
}
}
#[test]
fn pseudo_class_to_css_dir() {
assert_eq!(css(&PseudoClass::Dir("ltr".into())), ":dir(ltr)");
}
#[test]
fn pseudo_class_is_active_or_hover() {
assert!(PseudoClass::Active.is_active_or_hover());
assert!(PseudoClass::Hover.is_active_or_hover());
assert!(!PseudoClass::Focus.is_active_or_hover());
assert!(!PseudoClass::Link.is_active_or_hover());
assert!(!PseudoClass::Target.is_active_or_hover());
}
#[test]
fn pseudo_class_is_user_action_state() {
assert!(PseudoClass::Active.is_user_action_state());
assert!(PseudoClass::Hover.is_user_action_state());
assert!(PseudoClass::Focus.is_user_action_state());
assert!(PseudoClass::FocusWithin.is_user_action_state());
assert!(PseudoClass::FocusVisible.is_user_action_state());
assert!(!PseudoClass::Link.is_user_action_state());
assert!(!PseudoClass::Target.is_user_action_state());
assert!(!PseudoClass::Enabled.is_user_action_state());
assert!(!PseudoClass::Checked.is_user_action_state());
}
}
#[cfg(test)]
mod early_exit_tests {
use super::*;
use proptest::prelude::*;
const CORPUS: &str = include_str!("../../tests/corpus/selectors.txt");
fn reference(
css: &str,
default_namespace: Option<&str>,
) -> Result<SelectorList<CssToXpathImpl>, Error> {
let default_namespace = CssString::from(default_namespace.unwrap_or(""));
let strict = Attempt {
forgiving: false,
default_namespace: &default_namespace,
};
let forgiving = Attempt {
forgiving: true,
..strict
};
let strict_error = match strict.parse(css) {
Ok(list) => return Ok(list),
Err(e) => e,
};
match forgiving.parse(css) {
Ok(list) if dropped_nothing(&list) => Ok(list),
Ok(_) => Err(parse_error(css, strict, &strict_error)),
Err(e) if is_empty_selector(&strict_error) => Err(parse_error(css, forgiving, &e)),
Err(_) => Err(parse_error(css, strict, &strict_error)),
}
}
fn assert_agrees(css: &str) -> Result<(), TestCaseError> {
let shown =
|r: Result<SelectorList<CssToXpathImpl>, Error>| r.map(|list| list.to_css_string());
prop_assert_eq!(
shown(parse_lists(css, None)),
shown(reference(css, None)),
"{:?}",
css
);
Ok(())
}
const PIECES: &[&str] = &[
":is(",
":where(",
":not(",
":has(",
":nth-child(",
"::before",
":foo",
"(",
")",
"[",
"]",
",",
" ",
">",
"+",
"~",
"|",
"*",
"=",
"\"",
"'",
"\\",
"/**/",
"/*",
"#",
".",
"a",
"b",
"1",
"-",
"&",
"{",
"}",
"\u{e9}",
];
fn piece() -> impl Strategy<Value = String> {
proptest::sample::select(PIECES).prop_map(str::to_owned)
}
fn corpus_line() -> impl Strategy<Value = String> {
let lines: Vec<&'static str> = CORPUS.lines().collect();
proptest::sample::select(lines).prop_map(str::to_owned)
}
fn mutated() -> impl Strategy<Value = String> {
(
corpus_line(),
proptest::collection::vec((any::<proptest::sample::Index>(), piece()), 0..4),
proptest::collection::vec(any::<proptest::sample::Index>(), 0..3),
)
.prop_map(|(line, inserts, removals)| {
let mut chars: Vec<char> = line.chars().collect();
for index in removals {
if !chars.is_empty() {
chars.remove(index.index(chars.len()));
}
}
for (index, piece) in inserts {
let at = index.index(chars.len() + 1);
chars.splice(at..at, piece.chars());
}
chars.into_iter().collect()
})
}
#[test]
fn corpus_agrees() {
for line in CORPUS.lines() {
assert_agrees(line).unwrap();
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn mutated_corpus_agrees(css in mutated()) {
assert_agrees(&css)?;
}
#[test]
fn assembled_pieces_agree(pieces in proptest::collection::vec(piece(), 0..12)) {
assert_agrees(&pieces.concat())?;
}
}
}