pub mod impls;
use cssparser::{
Parser as CssParser, ParserInput, SourceLocation, ToCss, Token, match_ignore_ascii_case,
};
use selectors::parser::{
NonTSPseudoClass, ParseRelative, PseudoElement, SelectorImpl, SelectorList,
SelectorParseErrorKind,
};
use std::fmt;
pub use impls::CssString;
use crate::translate::error::Error;
#[derive(Clone, Debug)]
pub 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 enum LangArg {
Value(String),
Star,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PseudoClass {
AnyLink,
Link,
Visited,
Hover,
Active,
Focus,
FocusWithin,
FocusVisible,
Target,
TargetWithin,
LocalLink,
Enabled,
Disabled,
Checked,
Required,
Optional,
Lang(Vec<LangArg>),
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::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(args) => {
dest.write_char('(')?;
for (i, arg) in args.iter().enumerate() {
if i > 0 {
dest.write_char(' ')?;
}
match arg {
LangArg::Value(v) => cssparser::serialize_identifier(v, dest)?,
LangArg::Star => dest.write_char('*')?,
}
}
dest.write_char(')')
}
PseudoClass::Dir(value) => {
dest.write_char('(')?;
cssparser::serialize_identifier(value, dest)?;
dest.write_char(')')
}
_ => Ok(()),
}
}
}
impl NonTSPseudoClass for PseudoClass {
type Impl = CssToXpathImpl;
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 enum NeverPseudoElement {}
impl ToCss for NeverPseudoElement {
fn to_css<W: fmt::Write>(&self, _dest: &mut W) -> fmt::Result {
match *self {}
}
}
impl PseudoElement for NeverPseudoElement {
type Impl = CssToXpathImpl;
}
pub struct CssToXpathParser;
impl<'i> selectors::parser::Parser<'i> for CssToXpathParser {
type Impl = CssToXpathImpl;
type Error = SelectorParseErrorKind<'i>;
fn allow_forgiving_selectors(&self) -> bool {
false
}
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,
location: SourceLocation,
name: cssparser::CowRcStr<'i>,
) -> Result<PseudoClass, cssparser::ParseError<'i, 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,
_ => {
return Err(location.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
));
},
};
Ok(pc)
}
fn parse_non_ts_functional_pseudo_class<'t>(
&self,
name: cssparser::CowRcStr<'i>,
parser: &mut CssParser<'i, 't>,
_after_part: bool,
) -> Result<PseudoClass, cssparser::ParseError<'i, Self::Error>> {
if name.eq_ignore_ascii_case("dir") {
let value = match parser.next() {
Ok(Token::Ident(v)) => v.as_ref().to_owned(),
_ => {
return Err(parser.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
));
}
};
if parser.next().is_ok() {
return Err(parser.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
));
}
return Ok(PseudoClass::Dir(value));
}
if !name.eq_ignore_ascii_case("lang") {
return Err(parser.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
));
}
let mut args = Vec::new();
loop {
let token = match parser.next() {
Ok(t) => t.clone(),
Err(_) => break, };
match token {
Token::Ident(ref v) => args.push(LangArg::Value(v.as_ref().to_owned())),
Token::QuotedString(ref v) => args.push(LangArg::Value(v.as_ref().to_owned())),
Token::Delim('*') => args.push(LangArg::Star),
Token::Comma => {}
_ => {
return Err(parser.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
));
}
}
}
if args.is_empty() {
return Err(parser.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
));
}
Ok(PseudoClass::Lang(args))
}
fn namespace_for_prefix(&self, prefix: &CssString) -> Option<CssString> {
Some(prefix.clone())
}
fn default_namespace(&self) -> Option<CssString> {
Some(CssString::from(""))
}
}
fn uses_column_combinator(css: &str) -> bool {
let bytes = css.as_bytes();
let mut i = 0;
let mut quote: Option<u8> = 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 => 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'|') => return true,
_ => {}
},
}
i += 1;
}
false
}
pub fn parse(css: &str) -> Result<SelectorList<CssToXpathImpl>, Error> {
if uses_column_combinator(css) {
return Err(Error::Unsupported("the `||` column combinator".into()));
}
let mut input = ParserInput::new(css);
let mut parser = CssParser::new(&mut input);
SelectorList::parse(&CssToXpathParser, &mut parser, ParseRelative::No).map_err(|e| {
let detail = match e.kind {
cssparser::ParseErrorKind::Basic(ref kind) => format!("{kind:?}"),
cssparser::ParseErrorKind::Custom(ref kind) => format!("{kind:?}"),
};
Error::Parse(detail, e.location.column)
})
}