434 lines
12 KiB
Rust
434 lines
12 KiB
Rust
use crate::attributes::ExpandedName;
|
|
use cssparser::{self, CowRcStr, ParseError, SourceLocation, ToCss};
|
|
use html5ever::{LocalName, Namespace};
|
|
use crate::iter::{NodeIterator, Select};
|
|
use crate::node_data_ref::NodeDataRef;
|
|
use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
|
|
use selectors::context::QuirksMode;
|
|
use selectors::parser::SelectorParseErrorKind;
|
|
use selectors::parser::{
|
|
NonTSPseudoClass, Parser, Selector as GenericSelector, SelectorImpl, SelectorList,
|
|
};
|
|
use selectors::{self, matching, OpaqueElement};
|
|
use std::fmt;
|
|
use crate::tree::{ElementData, Node, NodeData, NodeRef};
|
|
|
|
/// The definition of whitespace per CSS Selectors Level 3 § 4.
|
|
///
|
|
/// Copied from rust-selectors.
|
|
static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C'];
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct KuchikiSelectors;
|
|
|
|
impl SelectorImpl for KuchikiSelectors {
|
|
type AttrValue = String;
|
|
type Identifier = LocalName;
|
|
type ClassName = LocalName;
|
|
type LocalName = LocalName;
|
|
type PartName = LocalName;
|
|
type NamespacePrefix = LocalName;
|
|
type NamespaceUrl = Namespace;
|
|
type BorrowedNamespaceUrl = Namespace;
|
|
type BorrowedLocalName = LocalName;
|
|
|
|
type NonTSPseudoClass = PseudoClass;
|
|
type PseudoElement = PseudoElement;
|
|
|
|
type ExtraMatchingData = ();
|
|
}
|
|
|
|
struct KuchikiParser;
|
|
|
|
impl<'i> Parser<'i> for KuchikiParser {
|
|
type Impl = KuchikiSelectors;
|
|
type Error = SelectorParseErrorKind<'i>;
|
|
|
|
fn parse_non_ts_pseudo_class(
|
|
&self,
|
|
location: SourceLocation,
|
|
name: CowRcStr<'i>,
|
|
) -> Result<PseudoClass, ParseError<'i, SelectorParseErrorKind<'i>>> {
|
|
use self::PseudoClass::*;
|
|
if name.eq_ignore_ascii_case("any-link") {
|
|
Ok(AnyLink)
|
|
} else if name.eq_ignore_ascii_case("link") {
|
|
Ok(Link)
|
|
} else if name.eq_ignore_ascii_case("visited") {
|
|
Ok(Visited)
|
|
} else if name.eq_ignore_ascii_case("active") {
|
|
Ok(Active)
|
|
} else if name.eq_ignore_ascii_case("focus") {
|
|
Ok(Focus)
|
|
} else if name.eq_ignore_ascii_case("hover") {
|
|
Ok(Hover)
|
|
} else if name.eq_ignore_ascii_case("enabled") {
|
|
Ok(Enabled)
|
|
} else if name.eq_ignore_ascii_case("disabled") {
|
|
Ok(Disabled)
|
|
} else if name.eq_ignore_ascii_case("checked") {
|
|
Ok(Checked)
|
|
} else if name.eq_ignore_ascii_case("indeterminate") {
|
|
Ok(Indeterminate)
|
|
} else {
|
|
Err(
|
|
location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
|
|
name,
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
|
pub enum PseudoClass {
|
|
AnyLink,
|
|
Link,
|
|
Visited,
|
|
Active,
|
|
Focus,
|
|
Hover,
|
|
Enabled,
|
|
Disabled,
|
|
Checked,
|
|
Indeterminate,
|
|
}
|
|
|
|
impl NonTSPseudoClass for PseudoClass {
|
|
type Impl = KuchikiSelectors;
|
|
|
|
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)
|
|
}
|
|
|
|
fn has_zero_specificity(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ToCss for PseudoClass {
|
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
|
|
where
|
|
W: fmt::Write,
|
|
{
|
|
dest.write_str(match *self {
|
|
PseudoClass::AnyLink => ":any-link",
|
|
PseudoClass::Link => ":link",
|
|
PseudoClass::Visited => ":visited",
|
|
PseudoClass::Active => ":active",
|
|
PseudoClass::Focus => ":focus",
|
|
PseudoClass::Hover => ":hover",
|
|
PseudoClass::Enabled => ":enabled",
|
|
PseudoClass::Disabled => ":disabled",
|
|
PseudoClass::Checked => ":checked",
|
|
PseudoClass::Indeterminate => ":indeterminate",
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
|
pub enum PseudoElement {}
|
|
|
|
impl ToCss for PseudoElement {
|
|
fn to_css<W>(&self, _dest: &mut W) -> fmt::Result
|
|
where
|
|
W: fmt::Write,
|
|
{
|
|
match *self {}
|
|
}
|
|
}
|
|
|
|
impl selectors::parser::PseudoElement for PseudoElement {
|
|
type Impl = KuchikiSelectors;
|
|
}
|
|
|
|
impl selectors::Element for NodeDataRef<ElementData> {
|
|
type Impl = KuchikiSelectors;
|
|
|
|
#[inline]
|
|
fn opaque(&self) -> OpaqueElement {
|
|
let node: &Node = self.as_node();
|
|
OpaqueElement::new(node)
|
|
}
|
|
|
|
#[inline]
|
|
fn is_html_slot_element(&self) -> bool {
|
|
false
|
|
}
|
|
#[inline]
|
|
fn parent_node_is_shadow_root(&self) -> bool {
|
|
false
|
|
}
|
|
#[inline]
|
|
fn containing_shadow_host(&self) -> Option<Self> {
|
|
None
|
|
}
|
|
|
|
#[inline]
|
|
fn parent_element(&self) -> Option<Self> {
|
|
self.as_node().parent().and_then(NodeRef::into_element_ref)
|
|
}
|
|
#[inline]
|
|
fn prev_sibling_element(&self) -> Option<Self> {
|
|
self.as_node().preceding_siblings().elements().next()
|
|
}
|
|
#[inline]
|
|
fn next_sibling_element(&self) -> Option<Self> {
|
|
self.as_node().following_siblings().elements().next()
|
|
}
|
|
#[inline]
|
|
fn is_empty(&self) -> bool {
|
|
self.as_node().children().all(|child| match *child.data() {
|
|
NodeData::Element(_) => false,
|
|
NodeData::Text(ref text) => text.borrow().is_empty(),
|
|
_ => true,
|
|
})
|
|
}
|
|
#[inline]
|
|
fn is_root(&self) -> bool {
|
|
match self.as_node().parent() {
|
|
None => false,
|
|
Some(parent) => matches!(*parent.data(), NodeData::Document(_)),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn is_html_element_in_html_document(&self) -> bool {
|
|
// FIXME: Have a notion of HTML document v.s. XML document?
|
|
self.name.ns == ns!(html)
|
|
}
|
|
|
|
#[inline]
|
|
fn has_local_name(&self, name: &LocalName) -> bool {
|
|
self.name.local == *name
|
|
}
|
|
#[inline]
|
|
fn has_namespace(&self, namespace: &Namespace) -> bool {
|
|
self.name.ns == *namespace
|
|
}
|
|
|
|
#[inline]
|
|
fn is_part(&self, _name: &LocalName) -> bool {
|
|
false
|
|
}
|
|
|
|
#[inline]
|
|
fn exported_part(&self, _: &LocalName) -> Option<LocalName> {
|
|
None
|
|
}
|
|
|
|
#[inline]
|
|
fn imported_part(&self, _: &LocalName) -> Option<LocalName> {
|
|
None
|
|
}
|
|
|
|
#[inline]
|
|
fn is_pseudo_element(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
#[inline]
|
|
fn is_same_type(&self, other: &Self) -> bool {
|
|
self.name == other.name
|
|
}
|
|
|
|
#[inline]
|
|
fn is_link(&self) -> bool {
|
|
self.name.ns == ns!(html)
|
|
&& matches!(
|
|
self.name.local,
|
|
local_name!("a") | local_name!("area") | local_name!("link")
|
|
)
|
|
&& self
|
|
.attributes
|
|
.borrow()
|
|
.map
|
|
.contains_key(&ExpandedName::new(ns!(), local_name!("href")))
|
|
}
|
|
|
|
#[inline]
|
|
fn has_id(&self, id: &LocalName, case_sensitivity: CaseSensitivity) -> bool {
|
|
self.attributes
|
|
.borrow()
|
|
.get(local_name!("id"))
|
|
.map_or(false, |id_attr| {
|
|
case_sensitivity.eq(id.as_bytes(), id_attr.as_bytes())
|
|
})
|
|
}
|
|
|
|
#[inline]
|
|
fn has_class(&self, name: &LocalName, case_sensitivity: CaseSensitivity) -> bool {
|
|
let name = name.as_bytes();
|
|
!name.is_empty()
|
|
&& if let Some(class_attr) = self.attributes.borrow().get(local_name!("class")) {
|
|
class_attr
|
|
.split(SELECTOR_WHITESPACE)
|
|
.any(|class| case_sensitivity.eq(class.as_bytes(), name))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn attr_matches(
|
|
&self,
|
|
ns: &NamespaceConstraint<&Namespace>,
|
|
local_name: &LocalName,
|
|
operation: &AttrSelectorOperation<&String>,
|
|
) -> bool {
|
|
let attrs = self.attributes.borrow();
|
|
match *ns {
|
|
NamespaceConstraint::Any => attrs
|
|
.map
|
|
.iter()
|
|
.any(|(name, attr)| name.local == *local_name && operation.eval_str(&attr.value)),
|
|
NamespaceConstraint::Specific(ns_url) => attrs
|
|
.map
|
|
.get(&ExpandedName::new(ns_url, local_name.clone()))
|
|
.map_or(false, |attr| operation.eval_str(&attr.value)),
|
|
}
|
|
}
|
|
|
|
fn match_pseudo_element(
|
|
&self,
|
|
pseudo: &PseudoElement,
|
|
_context: &mut matching::MatchingContext<KuchikiSelectors>,
|
|
) -> bool {
|
|
match *pseudo {}
|
|
}
|
|
|
|
fn match_non_ts_pseudo_class<F>(
|
|
&self,
|
|
pseudo: &PseudoClass,
|
|
_context: &mut matching::MatchingContext<KuchikiSelectors>,
|
|
_flags_setter: &mut F,
|
|
) -> bool
|
|
where
|
|
F: FnMut(&Self, matching::ElementSelectorFlags),
|
|
{
|
|
use self::PseudoClass::*;
|
|
match *pseudo {
|
|
Active | Focus | Hover | Enabled | Disabled | Checked | Indeterminate | Visited => {
|
|
false
|
|
}
|
|
AnyLink | Link => {
|
|
self.name.ns == ns!(html)
|
|
&& matches!(
|
|
self.name.local,
|
|
local_name!("a") | local_name!("area") | local_name!("link")
|
|
)
|
|
&& self.attributes.borrow().contains(local_name!("href"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A pre-compiled list of CSS Selectors.
|
|
pub struct Selectors(pub Vec<Selector>);
|
|
|
|
/// A pre-compiled CSS Selector.
|
|
pub struct Selector(GenericSelector<KuchikiSelectors>);
|
|
|
|
/// The specificity of a selector.
|
|
///
|
|
/// Opaque, but ordered.
|
|
///
|
|
/// Determines precedence in the cascading algorithm.
|
|
/// When equal, a rule later in source order takes precedence.
|
|
#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
|
pub struct Specificity(u32);
|
|
|
|
impl Selectors {
|
|
/// Compile a list of selectors. This may fail on syntax errors or unsupported selectors.
|
|
#[inline]
|
|
pub fn compile(s: &str) -> Result<Selectors, ()> {
|
|
let mut input = cssparser::ParserInput::new(s);
|
|
match SelectorList::parse(&KuchikiParser, &mut cssparser::Parser::new(&mut input)) {
|
|
Ok(list) => Ok(Selectors(list.0.into_iter().map(Selector).collect())),
|
|
Err(_) => Err(()),
|
|
}
|
|
}
|
|
|
|
/// Returns whether the given element matches this list of selectors.
|
|
#[inline]
|
|
pub fn matches(&self, element: &NodeDataRef<ElementData>) -> bool {
|
|
self.0.iter().any(|s| s.matches(element))
|
|
}
|
|
|
|
/// Filter an element iterator, yielding those matching this list of selectors.
|
|
#[inline]
|
|
pub fn filter<I>(&self, iter: I) -> Select<I, &Selectors>
|
|
where
|
|
I: Iterator<Item = NodeDataRef<ElementData>>,
|
|
{
|
|
Select {
|
|
iter,
|
|
selectors: self,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Selector {
|
|
/// Returns whether the given element matches this selector.
|
|
#[inline]
|
|
pub fn matches(&self, element: &NodeDataRef<ElementData>) -> bool {
|
|
let mut context = matching::MatchingContext::new(
|
|
matching::MatchingMode::Normal,
|
|
None,
|
|
None,
|
|
QuirksMode::NoQuirks,
|
|
);
|
|
matching::matches_selector(&self.0, 0, None, element, &mut context, &mut |_, _| {})
|
|
}
|
|
|
|
/// Return the specificity of this selector.
|
|
pub fn specificity(&self) -> Specificity {
|
|
Specificity(self.0.specificity())
|
|
}
|
|
}
|
|
|
|
impl ::std::str::FromStr for Selectors {
|
|
type Err = ();
|
|
#[inline]
|
|
fn from_str(s: &str) -> Result<Selectors, ()> {
|
|
Selectors::compile(s)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Selector {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
self.0.to_css(f)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Selectors {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut iter = self.0.iter();
|
|
let first = iter
|
|
.next()
|
|
.expect("Empty Selectors, should contain at least one selector");
|
|
first.0.to_css(f)?;
|
|
for selector in iter {
|
|
f.write_str(", ")?;
|
|
selector.0.to_css(f)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Selector {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt::Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Selectors {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt::Display::fmt(self, f)
|
|
}
|
|
}
|