mod impls;
use cssparser::{
Parser as CssParser, ParserInput, SourceLocation, 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(", ")?;
}
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 {
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(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 {
type Impl = CssToXpathImpl;
}
pub(crate) struct CssToXpathParser<'a> {
forgiving: bool,
default_namespace: Option<&'a str>,
}
impl<'i> selectors::parser::Parser<'i> for CssToXpathParser<'_> {
type Impl = CssToXpathImpl;
type Error = SelectorParseErrorKind<'i>;
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,
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,
"read-only" => PseudoClass::ReadOnly,
"read-write" => PseudoClass::ReadWrite,
"default" => PseudoClass::Default,
"placeholder-shown" => PseudoClass::PlaceholderShown,
_ => {
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),
));
}
match parse_lang_ranges(parser) {
Some(ranges) => Ok(PseudoClass::Lang(ranges)),
None => Err(parser.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
)),
}
}
fn namespace_for_prefix(&self, prefix: &CssString) -> Option<CssString> {
Some(prefix.clone())
}
fn default_namespace(&self) -> Option<CssString> {
Some(CssString::from(self.default_namespace.unwrap_or("")))
}
}
fn parse_lang_ranges<'i>(parser: &mut CssParser<'i, '_>) -> Option<Vec<String>> {
let mut ranges: Vec<String> = Vec::new();
let mut current = String::new();
let mut started = false;
let mut adjacent = true;
loop {
let token = match parser.next_including_whitespace_and_comments() {
Ok(t) => t.clone(),
Err(_) => break, };
let piece = match token {
Token::WhiteSpace(_) | Token::Comment(_) => {
adjacent = false;
continue;
}
Token::Comma => {
if !started || !is_valid_lang_range(¤t) {
return None;
}
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 None,
};
if started && !adjacent {
return None; }
current.push_str(&piece);
started = true;
}
if !started || !is_valid_lang_range(¤t) {
return None; }
ranges.push(current);
Some(ranges)
}
fn is_valid_lang_range(range: &str) -> bool {
!range.is_empty()
&& range
.split('-')
.all(|subtag| !subtag.is_empty() && (subtag == "*" || !subtag.contains('*')))
}
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<'i> = cssparser::ParseError<'i, SelectorParseErrorKind<'i>>;
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 strict = match parse_list(css, false, default_namespace) {
Ok(list) => return Ok(list),
Err(e) => e,
};
match parse_list(css, true, default_namespace) {
Ok(list) if dropped_nothing(&list) => Ok(list),
Ok(_) => Err(parse_error(css, &strict)),
Err(e) if is_empty_selector(&strict) => Err(parse_error(css, &e)),
Err(_) => Err(parse_error(css, &strict)),
}
}
fn parse_list<'i>(
css: &'i str,
forgiving: bool,
default_namespace: Option<&str>,
) -> Result<SelectorList<CssToXpathImpl>, ParseFailure<'i>> {
let mut input = ParserInput::new(css);
let mut parser = CssParser::new(&mut input);
SelectorList::parse(
&CssToXpathParser {
forgiving,
default_namespace,
},
&mut parser,
ParseRelative::No,
)
}
fn parse_error(css: &str, e: &ParseFailure<'_>) -> Error {
Error::Parse {
kind: ParseErrorKind::from_kind(&e.kind),
offset: byte_offset(css, e.location),
}
}
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;
}
let mut input = ParserInput::new(source.as_str());
CssParser::new(&mut input).is_exhausted()
}
fn byte_offset(css: &str, location: SourceLocation) -> usize {
let bytes = css.as_bytes();
let mut offset = 0;
let mut line = 0;
while line < location.line && offset < bytes.len() {
match bytes[offset] {
b'\r' => {
offset += 1;
if bytes.get(offset) == Some(&b'\n') {
offset += 1;
}
line += 1;
}
b'\n' | b'\x0C' => {
offset += 1;
line += 1;
}
_ => offset += 1,
}
}
let mut units = location.column.saturating_sub(1);
for c in css[offset..].chars() {
if units == 0 {
break;
}
units = units.saturating_sub(c.len_utf16() as u32);
offset += c.len_utf8();
}
offset
}
#[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)");
}
#[test]
fn lang_range_grammar() {
fn ranges(css: &str) -> Option<Vec<String>> {
let mut input = ParserInput::new(css);
let mut parser = CssParser::new(&mut input);
parser.expect_function_matching("lang").ok()?;
parser
.parse_nested_block(|p| {
Ok::<_, cssparser::ParseError<'_, ()>>(parse_lang_ranges(p))
})
.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");
assert_eq!(
ranges("lang( en , fr )"),
Some(vec!["en".to_owned(), "fr".to_owned()])
);
for css in [
"lang()",
"lang(en fr)", "lang(en *)", "lang(en*)", "lang(*en)",
"lang(\"\")",
"lang(en-)",
"lang(--x)",
"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());
}
}