use memchr::{memchr as find_char, memmem, memrchr as find_char_reverse};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::resources::PermissionMask;
use crate::utils::Hash;
use css_validation::{is_valid_css_style, validate_css_selector};
#[derive(Debug, Error, PartialEq)]
pub enum CosmeticFilterError {
#[error("punycode error")]
PunycodeError,
#[error("invalid action specifier")]
InvalidActionSpecifier,
#[error("unsupported syntax")]
UnsupportedSyntax,
#[error("missing sharp")]
MissingSharp,
#[error("invalid css style")]
InvalidCssStyle,
#[error("invalid css selector")]
InvalidCssSelector,
#[error("generic unhide")]
GenericUnhide,
#[error("generic script inject")]
GenericScriptInject,
#[error("procedural and action filters cannot be generic")]
GenericAction,
#[error("double negation")]
DoubleNegation,
#[error("empty rule")]
EmptyRule,
#[error("html filtering is unsupported")]
HtmlFilteringUnsupported,
#[error("scriptlet args could not be parsed")]
InvalidScriptletArgs,
#[error("location modifiers are unsupported")]
LocationModifiersUnsupported,
#[error("procedural filters can only accept a single CSS selector")]
ProceduralFilterWithMultipleSelectors,
}
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "arg")]
#[serde(rename_all = "kebab-case")]
pub enum CosmeticFilterAction {
Remove,
Style(String),
RemoveAttr(String),
RemoveClass(String),
}
impl CosmeticFilterAction {
fn new_style(style: &str) -> Result<Self, CosmeticFilterError> {
if !is_valid_css_style(style) {
return Err(CosmeticFilterError::InvalidCssStyle);
}
Ok(Self::Style(style.to_string()))
}
fn new_remove_attr(attr: &str) -> Result<Self, CosmeticFilterError> {
Self::forbid_regex_or_quoted_args(attr)?;
Ok(CosmeticFilterAction::RemoveAttr(attr.to_string()))
}
fn new_remove_class(class: &str) -> Result<Self, CosmeticFilterError> {
Self::forbid_regex_or_quoted_args(class)?;
Ok(CosmeticFilterAction::RemoveClass(class.to_string()))
}
fn forbid_regex_or_quoted_args(arg: &str) -> Result<(), CosmeticFilterError> {
if arg.starts_with('/') || arg.starts_with('\"') || arg.starts_with('\'') {
return Err(CosmeticFilterError::UnsupportedSyntax);
}
Ok(())
}
}
bitflags::bitflags! {
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(transparent)]
pub struct CosmeticFilterMask: u8 {
const UNHIDE = 1 << 0;
const SCRIPT_INJECT = 1 << 1;
const NONE = 0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CosmeticFilter {
pub entities: Option<Vec<Hash>>,
pub hostnames: Option<Vec<Hash>>,
pub mask: CosmeticFilterMask,
pub not_entities: Option<Vec<Hash>>,
pub not_hostnames: Option<Vec<Hash>>,
pub raw_line: Option<Box<String>>,
pub selector: Vec<CosmeticFilterOperator>,
pub action: Option<CosmeticFilterAction>,
pub permission: PermissionMask,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "arg")]
#[serde(rename_all = "kebab-case")]
pub enum CosmeticFilterOperator {
CssSelector(String),
HasText(String),
MatchesAttr(String),
MatchesCss(String),
MatchesCssBefore(String),
MatchesCssAfter(String),
MatchesPath(String),
MinTextLength(String),
Upward(String),
Xpath(String),
}
pub(crate) enum CosmeticFilterLocationType {
Entity,
NotEntity,
Hostname,
NotHostname,
Unsupported,
}
#[derive(Default)]
struct CosmeticFilterLocations {
entities: Option<Vec<Hash>>,
not_entities: Option<Vec<Hash>>,
hostnames: Option<Vec<Hash>>,
not_hostnames: Option<Vec<Hash>>,
}
impl CosmeticFilter {
#[inline]
pub(crate) fn locations_before_sharp(
line: &str,
sharp_index: usize,
) -> impl Iterator<Item = (CosmeticFilterLocationType, &str)> {
line[0..sharp_index].split(',').filter_map(|part| {
if part.is_empty() {
return None;
}
let hostname = part;
let negation = hostname.starts_with('~');
let entity = hostname.ends_with(".*");
let start = if negation { 1 } else { 0 };
let end = if entity {
hostname.len() - 2
} else {
hostname.len()
};
let location = &hostname[start..end];
if location.starts_with('/') {
return Some((CosmeticFilterLocationType::Unsupported, part));
}
Some(match (negation, entity) {
(true, true) => (CosmeticFilterLocationType::NotEntity, location),
(true, false) => (CosmeticFilterLocationType::NotHostname, location),
(false, true) => (CosmeticFilterLocationType::Entity, location),
(false, false) => (CosmeticFilterLocationType::Hostname, location),
})
})
}
#[inline]
fn parse_before_sharp(
line: &str,
sharp_index: usize,
) -> Result<CosmeticFilterLocations, CosmeticFilterError> {
let mut entities_vec = vec![];
let mut not_entities_vec = vec![];
let mut hostnames_vec = vec![];
let mut not_hostnames_vec = vec![];
if line.starts_with('[') {
return Err(CosmeticFilterError::LocationModifiersUnsupported);
}
let mut any_unsupported = false;
for (location_type, location) in Self::locations_before_sharp(line, sharp_index) {
let mut hostname = String::new();
if location.is_ascii() {
hostname.push_str(location);
} else {
match idna::domain_to_ascii(location) {
Ok(x) if !x.is_empty() => hostname.push_str(&x),
_ => return Err(CosmeticFilterError::PunycodeError),
}
}
let hash = crate::utils::fast_hash(&hostname);
match location_type {
CosmeticFilterLocationType::NotEntity => not_entities_vec.push(hash),
CosmeticFilterLocationType::NotHostname => not_hostnames_vec.push(hash),
CosmeticFilterLocationType::Entity => entities_vec.push(hash),
CosmeticFilterLocationType::Hostname => hostnames_vec.push(hash),
CosmeticFilterLocationType::Unsupported => {
any_unsupported = true;
}
}
}
if any_unsupported
&& hostnames_vec.is_empty()
&& entities_vec.is_empty()
&& not_hostnames_vec.is_empty()
&& not_entities_vec.is_empty()
{
return Err(CosmeticFilterError::UnsupportedSyntax);
}
#[inline]
fn sorted_or_none<T: std::cmp::Ord>(mut vec: Vec<T>) -> Option<Vec<T>> {
if !vec.is_empty() {
vec.sort();
Some(vec)
} else {
None
}
}
let entities = sorted_or_none(entities_vec);
let hostnames = sorted_or_none(hostnames_vec);
let not_entities = sorted_or_none(not_entities_vec);
let not_hostnames = sorted_or_none(not_hostnames_vec);
Ok(CosmeticFilterLocations {
entities,
not_entities,
hostnames,
not_hostnames,
})
}
#[inline]
fn parse_after_sharp_nonscript(
after_sharp: &str,
) -> Result<(&str, Option<CosmeticFilterAction>), CosmeticFilterError> {
if after_sharp.starts_with('^') {
return Err(CosmeticFilterError::HtmlFilteringUnsupported);
}
const STYLE_TOKEN: &[u8] = b":style(";
const REMOVE_ATTR_TOKEN: &[u8] = b":remove-attr(";
const REMOVE_CLASS_TOKEN: &[u8] = b":remove-class(";
const REMOVE_TOKEN: &str = ":remove()";
type PairType = (
&'static [u8],
fn(&str) -> Result<CosmeticFilterAction, CosmeticFilterError>,
);
const PAIRS: &[PairType] = &[
(STYLE_TOKEN, CosmeticFilterAction::new_style),
(REMOVE_ATTR_TOKEN, CosmeticFilterAction::new_remove_attr),
(REMOVE_CLASS_TOKEN, CosmeticFilterAction::new_remove_class),
];
let action;
let selector;
'init: {
for (token, constructor) in PAIRS {
if let Some(i) = memmem::find(after_sharp.as_bytes(), token) {
if after_sharp.ends_with(')') {
let arg = &after_sharp[i + token.len()..after_sharp.len() - 1];
action = Some(constructor(arg)?);
selector = &after_sharp[..i];
break 'init;
} else {
return Err(CosmeticFilterError::InvalidActionSpecifier);
}
}
}
if let Some(before_suffix) = after_sharp.strip_suffix(REMOVE_TOKEN) {
action = Some(CosmeticFilterAction::Remove);
selector = before_suffix;
break 'init;
} else {
action = None;
selector = after_sharp;
}
}
Ok((selector, action))
}
pub fn plain_css_selector(&self) -> Option<&str> {
assert!(!self.selector.is_empty());
if self.selector.len() > 1 {
return None;
}
match &self.selector[0] {
CosmeticFilterOperator::CssSelector(s) => Some(s),
_ => None,
}
}
pub fn parse(
line: &str,
debug: bool,
permission: PermissionMask,
) -> Result<CosmeticFilter, CosmeticFilterError> {
let mut mask = CosmeticFilterMask::NONE;
if let Some(sharp_index) = find_char(b'#', line.as_bytes()) {
let after_sharp_index = sharp_index + 1;
let second_sharp_index = match find_char(b'#', &line.as_bytes()[after_sharp_index..]) {
Some(i) => i + after_sharp_index,
None => return Err(CosmeticFilterError::UnsupportedSyntax),
};
let mut translate_abp_syntax = false;
let mut between_sharps = &line[after_sharp_index..second_sharp_index];
if between_sharps.starts_with('@') {
if sharp_index == 0 {
return Err(CosmeticFilterError::GenericUnhide);
}
mask |= CosmeticFilterMask::UNHIDE;
between_sharps = &between_sharps[1..];
}
if between_sharps.starts_with('%') {
return Err(CosmeticFilterError::UnsupportedSyntax);
}
if between_sharps.starts_with('$') {
return Err(CosmeticFilterError::UnsupportedSyntax);
}
if between_sharps.starts_with('?') {
translate_abp_syntax = true;
between_sharps = &between_sharps[1..];
}
if !between_sharps.is_empty() {
return Err(CosmeticFilterError::UnsupportedSyntax);
}
let suffix_start_index = second_sharp_index + 1;
let CosmeticFilterLocations {
entities,
not_entities,
hostnames,
not_hostnames,
} = if sharp_index > 0 {
CosmeticFilter::parse_before_sharp(line, sharp_index)?
} else {
CosmeticFilterLocations::default()
};
let after_sharp = &line[suffix_start_index..].trim();
if after_sharp.is_empty() {
return Err(CosmeticFilterError::EmptyRule);
}
let (selector, action) = if line.len() - suffix_start_index > 4
&& line[suffix_start_index..].starts_with("+js(")
&& line.ends_with(')')
{
if sharp_index == 0 {
return Err(CosmeticFilterError::GenericScriptInject);
}
let args = &line[suffix_start_index + 4..line.len() - 1];
if crate::resources::parse_scriptlet_args(args).is_none() {
return Err(CosmeticFilterError::InvalidScriptletArgs);
}
mask |= CosmeticFilterMask::SCRIPT_INJECT;
(
vec![CosmeticFilterOperator::CssSelector(String::from(
&line[suffix_start_index + 4..line.len() - 1],
))],
None,
)
} else {
let (selector, action) = CosmeticFilter::parse_after_sharp_nonscript(after_sharp)?;
let validated_selector = validate_css_selector(selector, translate_abp_syntax)?;
if sharp_index == 0 && action.is_some() {
return Err(CosmeticFilterError::GenericAction);
}
(validated_selector, action)
};
if (not_entities.is_some() || not_hostnames.is_some())
&& mask.contains(CosmeticFilterMask::UNHIDE)
{
return Err(CosmeticFilterError::DoubleNegation);
}
let this = Self {
entities,
hostnames,
mask,
not_entities,
not_hostnames,
raw_line: if debug {
Some(Box::new(String::from(line)))
} else {
None
},
selector,
action,
permission,
};
if !this.has_hostname_constraint() && this.plain_css_selector().is_none() {
return Err(CosmeticFilterError::GenericAction);
}
Ok(this)
} else {
Err(CosmeticFilterError::MissingSharp)
}
}
pub fn has_hostname_constraint(&self) -> bool {
self.hostnames.is_some()
|| self.entities.is_some()
|| self.not_entities.is_some()
|| self.not_hostnames.is_some()
}
pub fn hidden_generic_rule(&self) -> Option<CosmeticFilter> {
if self.hostnames.is_some() || self.entities.is_some() {
None
} else if (self.not_hostnames.is_some() || self.not_entities.is_some())
&& (self.action.is_none() && !self.mask.contains(CosmeticFilterMask::SCRIPT_INJECT))
{
let mut generic_rule = self.clone();
generic_rule.not_hostnames = None;
generic_rule.not_entities = None;
Some(generic_rule)
} else {
None
}
}
}
fn get_hostname_without_public_suffix<'a>(
hostname: &'a str,
domain: &str,
) -> Option<(&'a str, &'a str)> {
let index_of_dot = find_char(b'.', domain.as_bytes());
if let Some(index_of_dot) = index_of_dot {
let public_suffix = &domain[index_of_dot + 1..];
Some((
&hostname[0..hostname.len() - public_suffix.len() - 1],
&hostname[hostname.len() - domain.len() + index_of_dot + 1..],
))
} else {
None
}
}
fn get_hashes_from_labels(hostname: &str, end: usize, start_of_domain: usize) -> Vec<Hash> {
let mut hashes = vec![];
if end == 0 {
return hashes;
}
let mut dot_ptr = start_of_domain;
while let Some(dot_index) = find_char_reverse(b'.', &hostname.as_bytes()[..dot_ptr]) {
dot_ptr = dot_index;
hashes.push(crate::utils::fast_hash(&hostname[dot_ptr + 1..end]));
}
hashes.push(crate::utils::fast_hash(&hostname[..end]));
hashes
}
pub(crate) fn get_entity_hashes_from_labels(hostname: &str, domain: &str) -> Vec<Hash> {
if let Some((hostname_without_public_suffix, public_suffix)) =
get_hostname_without_public_suffix(hostname, domain)
{
let mut hashes = get_hashes_from_labels(
hostname_without_public_suffix,
hostname_without_public_suffix.len(),
hostname_without_public_suffix.len(),
);
hashes.push(crate::utils::fast_hash(public_suffix));
hashes
} else {
vec![]
}
}
pub(crate) fn get_hostname_hashes_from_labels(hostname: &str, domain: &str) -> Vec<Hash> {
get_hashes_from_labels(hostname, hostname.len(), hostname.len() - domain.len())
}
#[cfg(not(feature = "css-validation"))]
mod css_validation {
use super::{CosmeticFilterError, CosmeticFilterOperator};
pub fn validate_css_selector(
selector: &str,
_accept_abp_selectors: bool,
) -> Result<Vec<CosmeticFilterOperator>, CosmeticFilterError> {
Ok(vec![CosmeticFilterOperator::CssSelector(
selector.to_string(),
)])
}
pub fn is_valid_css_style(_style: &str) -> bool {
true
}
}
#[cfg(feature = "css-validation")]
mod css_validation {
use super::{CosmeticFilterError, CosmeticFilterOperator};
use core::fmt::{Result as FmtResult, Write};
use cssparser::{CowRcStr, ParseError, Parser, ParserInput, SourceLocation, ToCss, Token};
use precomputed_hash::PrecomputedHash;
use selectors::parser::SelectorParseErrorKind;
pub fn validate_css_selector(
selector: &str,
accept_abp_selectors: bool,
) -> Result<Vec<CosmeticFilterOperator>, CosmeticFilterError> {
use regex::Regex;
use std::sync::LazyLock;
static RE_SIMPLE_SELECTOR: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[#.]?[A-Za-z_][\w-]*$").unwrap());
if RE_SIMPLE_SELECTOR.is_match(selector) {
return Ok(vec![CosmeticFilterOperator::CssSelector(
selector.to_string(),
)]);
}
let mock_stylesheet = format!("{selector}{{mock-stylesheet-marker}}");
let mut pi = ParserInput::new(&mock_stylesheet);
let mut parser = Parser::new(&mut pi);
let mut parser_impl = QualifiedRuleParserImpl {
accept_abp_selectors,
};
let mut rule_list_parser = cssparser::StyleSheetParser::new(&mut parser, &mut parser_impl);
let prelude = rule_list_parser.next().and_then(|r| r.ok());
if rule_list_parser.next().is_some() {
return Err(CosmeticFilterError::InvalidCssSelector);
}
fn has_procedural_operator(selector: &selectors::parser::Selector<SelectorImpl>) -> bool {
let mut iter = selector.iter();
loop {
for component in iter.by_ref() {
if is_procedural_operator(component) {
return true;
}
}
if iter.next_sequence().is_none() {
break false;
}
}
}
fn is_procedural_operator(c: &selectors::parser::Component<SelectorImpl>) -> bool {
use selectors::parser::Component;
matches!(
c,
Component::NonTSPseudoClass(NonTSPseudoClass::HasText(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::MatchesAttr(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::MatchesCss(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::MatchesCssBefore(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::MatchesCssAfter(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::MatchesPath(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::MinTextLength(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::Upward(_))
| Component::NonTSPseudoClass(NonTSPseudoClass::Xpath(_))
)
}
if let Some(prelude) = prelude {
if !prelude.slice().iter().any(has_procedural_operator) {
return Ok(vec![CosmeticFilterOperator::CssSelector(
prelude.to_css_string(),
)]);
}
if prelude.slice().len() != 1 {
return Err(CosmeticFilterError::ProceduralFilterWithMultipleSelectors);
}
let selector = prelude.slice().iter().next().unwrap();
enum SelectorsPart<'a> {
Component(&'a selectors::parser::Component<SelectorImpl>),
Combinator(selectors::parser::Combinator),
}
let mut parts = vec![];
let mut iter = selector.iter();
loop {
let mut components = vec![];
for component in iter.by_ref() {
components.push(SelectorsPart::Component(component));
}
parts.extend(components.into_iter().rev());
if let Some(combinator) = iter.next_sequence() {
parts.push(SelectorsPart::Combinator(combinator));
} else {
break;
}
}
let mut pending_css_selector = String::new();
let mut output = vec![];
for part in parts.into_iter().rev() {
use selectors::parser::Component;
match part {
SelectorsPart::Component(Component::NonTSPseudoClass(c)) => {
if let Some(procedural_operator) = c.to_procedural_operator() {
if !pending_css_selector.is_empty() {
output.push(CosmeticFilterOperator::CssSelector(
pending_css_selector,
));
pending_css_selector = String::new();
}
output.push(procedural_operator);
} else {
c.to_css(&mut pending_css_selector)
.map_err(|_| CosmeticFilterError::InvalidCssSelector)?;
}
}
SelectorsPart::Component(other) => {
other
.to_css(&mut pending_css_selector)
.map_err(|_| CosmeticFilterError::InvalidCssSelector)?;
}
SelectorsPart::Combinator(combinator) => {
combinator
.to_css(&mut pending_css_selector)
.map_err(|_| CosmeticFilterError::InvalidCssSelector)?;
}
}
}
if !pending_css_selector.is_empty() {
output.push(CosmeticFilterOperator::CssSelector(pending_css_selector));
}
Ok(output)
} else {
Err(CosmeticFilterError::InvalidCssSelector)
}
}
struct QualifiedRuleParserImpl {
accept_abp_selectors: bool,
}
impl<'i> cssparser::QualifiedRuleParser<'i> for QualifiedRuleParserImpl {
type Prelude = selectors::SelectorList<SelectorImpl>;
type QualifiedRule = selectors::SelectorList<SelectorImpl>;
type Error = ();
fn parse_prelude<'t>(
&mut self,
input: &mut Parser<'i, 't>,
) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
selectors::SelectorList::parse(
&SelectorParseImpl {
accept_abp_selectors: self.accept_abp_selectors,
},
input,
selectors::parser::ParseRelative::No,
)
.map_err(|_| ParseError {
kind: cssparser::ParseErrorKind::Custom(()),
location: SourceLocation { line: 0, column: 0 },
})
}
fn parse_block<'t>(
&mut self,
prelude: Self::Prelude,
_start: &cssparser::ParserState,
input: &mut Parser<'i, 't>,
) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
let err = Err(ParseError {
kind: cssparser::ParseErrorKind::Custom(()),
location: SourceLocation { line: 0, column: 0 },
});
match input.next() {
Ok(Token::Ident(i)) if i.as_ref() == "mock-stylesheet-marker" => (),
_ => return err,
}
if input.next().is_ok() {
return err;
}
Ok(prelude)
}
}
impl cssparser::AtRuleParser<'_> for QualifiedRuleParserImpl {
type Prelude = ();
type AtRule = selectors::SelectorList<SelectorImpl>;
type Error = ();
}
pub fn is_valid_css_style(style: &str) -> bool {
if style.contains('\\') {
return false;
}
if style.contains("url(") {
return false;
}
if style.contains("/*") {
return false;
}
true
}
struct SelectorParseImpl {
accept_abp_selectors: bool,
}
fn nested_matching_close(arg: &Token) -> Option<Token<'static>> {
match arg {
Token::Function(..) => Some(Token::CloseParenthesis),
Token::ParenthesisBlock => Some(Token::CloseParenthesis),
Token::CurlyBracketBlock => Some(Token::CloseCurlyBracket),
Token::SquareBracketBlock => Some(Token::CloseSquareBracket),
_ => None,
}
}
fn to_css_nested<'i>(
arguments: &mut Parser<'i, '_>,
) -> Result<String, ParseError<'i, SelectorParseErrorKind<'i>>> {
let mut inner = String::new();
while let Ok(arg) = arguments.next_including_whitespace() {
if arg.to_css(&mut inner).is_err() {
return Err(arguments.new_custom_error(SelectorParseErrorKind::InvalidState));
};
if let Some(closing_token) = nested_matching_close(arg) {
let nested = arguments.parse_nested_block(to_css_nested)?;
inner.push_str(&nested);
closing_token.to_css(&mut inner).map_err(|_| {
arguments.new_custom_error(SelectorParseErrorKind::InvalidState)
})?;
}
}
Ok(inner)
}
impl<'i> selectors::parser::Parser<'i> for SelectorParseImpl {
type Impl = SelectorImpl;
type Error = SelectorParseErrorKind<'i>;
fn parse_slotted(&self) -> bool {
true
}
fn parse_part(&self) -> bool {
true
}
fn parse_is_and_where(&self) -> bool {
true
}
fn parse_host(&self) -> bool {
true
}
fn parse_non_ts_pseudo_class(
&self,
_location: SourceLocation,
name: CowRcStr<'i>,
) -> Result<
<Self::Impl as selectors::parser::SelectorImpl>::NonTSPseudoClass,
ParseError<'i, Self::Error>,
> {
Ok(NonTSPseudoClass::AnythingElse(name.to_string(), None))
}
fn parse_non_ts_functional_pseudo_class<'t>(
&self,
name: CowRcStr<'i>,
arguments: &mut Parser<'i, 't>,
_after_part: bool,
) -> Result<
<Self::Impl as selectors::parser::SelectorImpl>::NonTSPseudoClass,
ParseError<'i, Self::Error>,
> {
let canonical_name = match (self.accept_abp_selectors, name.as_ref()) {
(true, "-abp-has") => Some("has"),
(true, "-abp-contains") => Some("has-text"),
(true, "contains") => Some("has-text"),
_ => None,
}
.unwrap_or(name.as_ref());
match canonical_name {
"has-text" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::HasText(text));
}
"matches-attr" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::MatchesAttr(text));
}
"matches-css" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::MatchesCss(text));
}
"matches-css-before" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::MatchesCssBefore(text));
}
"matches-css-after" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::MatchesCssAfter(text));
}
"matches-path" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::MatchesPath(text));
}
"min-text-length" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::MinTextLength(text));
}
"upward" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::Upward(text));
}
"xpath" => {
let text = to_css_nested(arguments)?;
return Ok(NonTSPseudoClass::Xpath(text));
}
"-abp-contains" | "-abp-has" | "-abp-properties" | "contains" | "if" | "if-not"
| "matches-property" | "nth-ancestor" | "properties" | "subject" | "remove"
| "remove-attr" | "remove-class" => {
return Err(arguments.new_custom_error(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
))
}
_ => (),
}
let inner_selector = to_css_nested(arguments)?;
Ok(NonTSPseudoClass::AnythingElse(
canonical_name.to_string(),
Some(inner_selector),
))
}
fn parse_pseudo_element(
&self,
_location: SourceLocation,
name: CowRcStr<'i>,
) -> Result<
<Self::Impl as selectors::parser::SelectorImpl>::PseudoElement,
ParseError<'i, Self::Error>,
> {
Ok(PseudoElement(name.to_string(), None))
}
fn parse_functional_pseudo_element<'t>(
&self,
name: CowRcStr<'i>,
arguments: &mut Parser<'i, 't>,
) -> Result<
<Self::Impl as selectors::parser::SelectorImpl>::PseudoElement,
ParseError<'i, Self::Error>,
> {
let inner_selector = to_css_nested(arguments)?;
Ok(PseudoElement(name.to_string(), Some(inner_selector)))
}
}
#[derive(Debug, Clone)]
struct SelectorImpl;
impl selectors::parser::SelectorImpl for SelectorImpl {
type ExtraMatchingData<'a> = ();
type AttrValue = CssString;
type Identifier = CssIdent;
type LocalName = CssIdent;
type NamespaceUrl = DummyValue;
type NamespacePrefix = DummyValue;
type BorrowedNamespaceUrl = DummyValue;
type BorrowedLocalName = CssIdent;
type NonTSPseudoClass = NonTSPseudoClass;
type PseudoElement = PseudoElement;
}
fn precomputed_hash_impl<H: std::hash::Hash>(this: &H) -> u32 {
use std::hash::{DefaultHasher, Hasher};
let mut hasher = DefaultHasher::new();
this.hash(&mut hasher);
hasher.finish() as u32
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct CssString(String);
impl ToCss for CssString {
fn to_css<W: Write>(&self, dest: &mut W) -> core::fmt::Result {
dest.write_str("\"")?;
{
let mut dest = cssparser::CssStringWriter::new(dest);
dest.write_str(&self.0)?;
}
dest.write_str("\"")
}
}
impl<'a> From<&'a str> for CssString {
fn from(s: &'a str) -> Self {
CssString(s.to_string())
}
}
impl PrecomputedHash for CssString {
fn precomputed_hash(&self) -> u32 {
precomputed_hash_impl(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct CssIdent(String);
impl ToCss for CssIdent {
fn to_css<W: Write>(&self, dest: &mut W) -> core::fmt::Result {
cssparser::serialize_identifier(&self.0, dest)
}
}
impl<'a> From<&'a str> for CssIdent {
fn from(s: &'a str) -> Self {
CssIdent(s.to_string())
}
}
impl PrecomputedHash for CssIdent {
fn precomputed_hash(&self) -> u32 {
precomputed_hash_impl(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct DummyValue(String);
impl ToCss for DummyValue {
fn to_css<W: Write>(&self, dest: &mut W) -> core::fmt::Result {
write!(dest, "{}", self.0)
}
}
impl<'a> From<&'a str> for DummyValue {
fn from(s: &'a str) -> Self {
DummyValue(s.to_string())
}
}
impl PrecomputedHash for DummyValue {
fn precomputed_hash(&self) -> u32 {
precomputed_hash_impl(&self.0)
}
}
#[derive(Clone, PartialEq, Eq)]
enum NonTSPseudoClass {
HasText(String),
MatchesAttr(String),
MatchesCss(String),
MatchesCssBefore(String),
MatchesCssAfter(String),
MatchesPath(String),
MinTextLength(String),
Upward(String),
Xpath(String),
AnythingElse(String, Option<String>),
}
impl selectors::parser::NonTSPseudoClass for NonTSPseudoClass {
type Impl = SelectorImpl;
fn is_active_or_hover(&self) -> bool {
false
}
fn is_user_action_state(&self) -> bool {
false
}
}
impl ToCss for NonTSPseudoClass {
fn to_css<W: Write>(&self, dest: &mut W) -> FmtResult {
write!(dest, ":")?;
match self {
Self::HasText(text) => write!(dest, "has-text({text})")?,
Self::MatchesAttr(text) => write!(dest, "matches-attr({text})")?,
Self::MatchesCss(text) => write!(dest, "matches-css({text})")?,
Self::MatchesCssBefore(text) => write!(dest, "matches-css-before({text})")?,
Self::MatchesCssAfter(text) => write!(dest, "matches-css-after({text})")?,
Self::MatchesPath(text) => write!(dest, "matches-path({text})")?,
Self::MinTextLength(text) => write!(dest, "min-text-length({text})")?,
Self::Upward(text) => write!(dest, "upward({text})")?,
Self::Xpath(text) => write!(dest, "xpath({text})")?,
Self::AnythingElse(name, None) => write!(dest, "{name}")?,
Self::AnythingElse(name, Some(args)) => write!(dest, "{name}({args})")?,
}
Ok(())
}
}
impl NonTSPseudoClass {
fn to_procedural_operator(&self) -> Option<CosmeticFilterOperator> {
match self {
NonTSPseudoClass::HasText(a) => Some(CosmeticFilterOperator::HasText(a.to_owned())),
NonTSPseudoClass::MatchesAttr(a) => {
Some(CosmeticFilterOperator::MatchesAttr(a.to_owned()))
}
NonTSPseudoClass::MatchesCss(a) => {
Some(CosmeticFilterOperator::MatchesCss(a.to_owned()))
}
NonTSPseudoClass::MatchesCssBefore(a) => {
Some(CosmeticFilterOperator::MatchesCssBefore(a.to_owned()))
}
NonTSPseudoClass::MatchesCssAfter(a) => {
Some(CosmeticFilterOperator::MatchesCssAfter(a.to_owned()))
}
NonTSPseudoClass::MatchesPath(a) => {
Some(CosmeticFilterOperator::MatchesPath(a.to_owned()))
}
NonTSPseudoClass::MinTextLength(a) => {
Some(CosmeticFilterOperator::MinTextLength(a.to_owned()))
}
NonTSPseudoClass::Upward(a) => Some(CosmeticFilterOperator::Upward(a.to_owned())),
NonTSPseudoClass::Xpath(a) => Some(CosmeticFilterOperator::Xpath(a.to_owned())),
_ => None,
}
}
}
#[derive(Clone, PartialEq, Eq)]
struct PseudoElement(String, Option<String>);
impl selectors::parser::PseudoElement for PseudoElement {
type Impl = SelectorImpl;
fn valid_after_slotted(&self) -> bool {
true
}
}
impl ToCss for PseudoElement {
fn to_css<W: Write>(&self, dest: &mut W) -> FmtResult {
write!(dest, "::")?;
match self {
Self(name, None) => write!(dest, "{name}")?,
Self(name, Some(args)) => write!(dest, "{name}({args})")?,
}
Ok(())
}
}
}
#[cfg(test)]
#[path = "../../tests/unit/filters/cosmetic.rs"]
mod unit_tests;