diff options
Diffstat (limited to 'components/selectors')
-rw-r--r-- | components/selectors/Cargo.toml | 5 | ||||
-rw-r--r-- | components/selectors/attr.rs | 190 | ||||
-rw-r--r-- | components/selectors/build.rs | 75 | ||||
-rw-r--r-- | components/selectors/lib.rs | 3 | ||||
-rw-r--r-- | components/selectors/matching.rs | 101 | ||||
-rw-r--r-- | components/selectors/parser.rs | 431 | ||||
-rw-r--r-- | components/selectors/size_of_tests.rs | 3 | ||||
-rw-r--r-- | components/selectors/tree.rs | 139 | ||||
-rw-r--r-- | components/selectors/visitor.rs | 11 |
9 files changed, 585 insertions, 373 deletions
diff --git a/components/selectors/Cargo.toml b/components/selectors/Cargo.toml index ceef176eae1..4ca9a240c67 100644 --- a/components/selectors/Cargo.toml +++ b/components/selectors/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/servo/servo" readme = "README.md" keywords = ["css", "selectors"] license = "MPL-2.0" +build = "build.rs" [lib] name = "selectors" @@ -25,8 +26,12 @@ bitflags = "0.7" matches = "0.1" cssparser = "0.13.3" fnv = "1.0" +phf = "0.7.18" precomputed-hash = "0.1" smallvec = "0.3" [dev-dependencies] size_of_test = {path = "../size_of_test"} + +[build-dependencies] +phf_codegen = "0.7.18" diff --git a/components/selectors/attr.rs b/components/selectors/attr.rs new file mode 100644 index 00000000000..274081ae9f7 --- /dev/null +++ b/components/selectors/attr.rs @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use cssparser::ToCss; +use parser::SelectorImpl; +use std::ascii::AsciiExt; +use std::fmt; + +#[derive(Eq, PartialEq, Clone)] +pub struct AttrSelectorWithNamespace<Impl: SelectorImpl> { + pub namespace: NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>, + pub local_name: Impl::LocalName, + pub local_name_lower: Impl::LocalName, + pub operation: ParsedAttrSelectorOperation<Impl::AttrValue>, + pub never_matches: bool, +} + +impl<Impl: SelectorImpl> AttrSelectorWithNamespace<Impl> { + pub fn namespace(&self) -> NamespaceConstraint<&Impl::NamespaceUrl> { + match self.namespace { + NamespaceConstraint::Any => NamespaceConstraint::Any, + NamespaceConstraint::Specific((_, ref url)) => { + NamespaceConstraint::Specific(url) + } + } + } +} + +#[derive(Eq, PartialEq, Clone)] +pub enum NamespaceConstraint<NamespaceUrl> { + Any, + + /// Empty string for no namespace + Specific(NamespaceUrl), +} + +#[derive(Eq, PartialEq, Clone)] +pub enum ParsedAttrSelectorOperation<AttrValue> { + Exists, + WithValue { + operator: AttrSelectorOperator, + case_sensitivity: ParsedCaseSensitivity, + expected_value: AttrValue, + } +} + +#[derive(Eq, PartialEq, Clone)] +pub enum AttrSelectorOperation<AttrValue> { + Exists, + WithValue { + operator: AttrSelectorOperator, + case_sensitivity: CaseSensitivity, + expected_value: AttrValue, + } +} + +impl<AttrValue> AttrSelectorOperation<AttrValue> { + pub fn eval_str(&self, element_attr_value: &str) -> bool where AttrValue: AsRef<str> { + match *self { + AttrSelectorOperation::Exists => true, + AttrSelectorOperation::WithValue { operator, case_sensitivity, ref expected_value } => { + operator.eval_str(element_attr_value, expected_value.as_ref(), case_sensitivity) + } + } + } +} + +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum AttrSelectorOperator { + Equal, + Includes, + DashMatch, + Prefix, + Substring, + Suffix, +} + +impl ToCss for AttrSelectorOperator { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + dest.write_str(match *self { + AttrSelectorOperator::Equal => " = ", + AttrSelectorOperator::Includes => " ~= ", + AttrSelectorOperator::DashMatch => " |= ", + AttrSelectorOperator::Prefix => " ^= ", + AttrSelectorOperator::Substring => " *= ", + AttrSelectorOperator::Suffix => " $= ", + }) + } +} + +impl AttrSelectorOperator { + pub fn eval_str(self, element_attr_value: &str, attr_selector_value: &str, + case_sensitivity: CaseSensitivity) -> bool { + let e = element_attr_value.as_bytes(); + let s = attr_selector_value.as_bytes(); + let case = case_sensitivity; + match self { + AttrSelectorOperator::Equal => { + case.eq(e, s) + } + AttrSelectorOperator::Prefix => { + e.len() >= s.len() && case.eq(&e[..s.len()], s) + } + AttrSelectorOperator::Suffix => { + e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s) + } + AttrSelectorOperator::Substring => { + case.contains(element_attr_value, attr_selector_value) + } + AttrSelectorOperator::Includes => { + element_attr_value.split(SELECTOR_WHITESPACE) + .any(|part| case.eq(part.as_bytes(), s)) + } + AttrSelectorOperator::DashMatch => { + case.eq(e, s) || ( + e.get(s.len()) == Some(&b'-') && + case.eq(&e[..s.len()], s) + ) + } + } + } +} + +/// The definition of whitespace per CSS Selectors Level 3 § 4. +pub static SELECTOR_WHITESPACE: &'static [char] = &[' ', '\t', '\n', '\r', '\x0C']; + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum ParsedCaseSensitivity { + CaseSensitive, // Selectors spec says language-defined, but HTML says sensitive. + AsciiCaseInsensitive, + AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument, +} + +impl ParsedCaseSensitivity { + pub fn to_unconditional(self, is_html_element_in_html_document: bool) -> CaseSensitivity { + match self { + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument + if is_html_element_in_html_document => { + CaseSensitivity::AsciiCaseInsensitive + } + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => { + CaseSensitivity::CaseSensitive + } + ParsedCaseSensitivity::CaseSensitive => CaseSensitivity::CaseSensitive, + ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive, + } + } +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum CaseSensitivity { + CaseSensitive, // Selectors spec says language-defined, but HTML says sensitive. + AsciiCaseInsensitive, +} + +impl CaseSensitivity { + pub fn eq(self, a: &[u8], b: &[u8]) -> bool { + match self { + CaseSensitivity::CaseSensitive => a == b, + CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), + } + } + + pub fn contains(self, haystack: &str, needle: &str) -> bool { + match self { + CaseSensitivity::CaseSensitive => haystack.contains(needle), + CaseSensitivity::AsciiCaseInsensitive => { + if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() { + haystack.bytes().enumerate().any(|(i, byte)| { + if !byte.eq_ignore_ascii_case(&n_first_byte) { + return false + } + let after_this_byte = &haystack.as_bytes()[i + 1..]; + match after_this_byte.get(..n_rest.len()) { + None => false, + Some(haystack_slice) => { + haystack_slice.eq_ignore_ascii_case(n_rest) + } + } + }) + } else { + // any_str.contains("") == true, + // though these cases should be handled with *NeverMatches and never go here. + true + } + } + } + } +} diff --git a/components/selectors/build.rs b/components/selectors/build.rs new file mode 100644 index 00000000000..0d0a40256f3 --- /dev/null +++ b/components/selectors/build.rs @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate phf_codegen; + +use std::env; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +fn main() { + let path = Path::new(&env::var("OUT_DIR").unwrap()) + .join("ascii_case_insensitive_html_attributes.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + + write!(&mut file, "{{ static SET: ::phf::Set<&'static str> = ", + ).unwrap(); + let mut set = phf_codegen::Set::new(); + for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() { + set.entry(name); + } + set.build(&mut file).unwrap(); + write!(&mut file, "; &SET }}").unwrap(); +} + +/// https://html.spec.whatwg.org/multipage/#selectors +static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &'static str = r#" + accept + accept-charset + align + alink + axis + bgcolor + charset + checked + clear + codetype + color + compact + declare + defer + dir + direction + disabled + enctype + face + frame + hreflang + http-equiv + lang + language + link + media + method + multiple + nohref + noresize + noshade + nowrap + readonly + rel + rev + rules + scope + scrolling + selected + shape + target + text + type + valign + valuetype + vlink +"#; diff --git a/components/selectors/lib.rs b/components/selectors/lib.rs index d352d2f1d3c..3e413c0c08b 100644 --- a/components/selectors/lib.rs +++ b/components/selectors/lib.rs @@ -6,11 +6,13 @@ #[macro_use] extern crate cssparser; #[macro_use] extern crate matches; extern crate fnv; +extern crate phf; extern crate precomputed_hash; #[cfg(test)] #[macro_use] extern crate size_of_test; extern crate smallvec; pub mod arcslice; +pub mod attr; pub mod bloom; pub mod matching; pub mod parser; @@ -21,4 +23,3 @@ pub mod visitor; pub use parser::{SelectorImpl, Parser, SelectorList}; pub use tree::Element; -pub use tree::{MatchAttr, MatchAttrGeneric}; diff --git a/components/selectors/matching.rs b/components/selectors/matching.rs index 9c60e6b78d7..50b686f2558 100644 --- a/components/selectors/matching.rs +++ b/components/selectors/matching.rs @@ -1,9 +1,11 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use attr::{ParsedAttrSelectorOperation, AttrSelectorOperation, NamespaceConstraint}; use bloom::BloomFilter; -use parser::{CaseSensitivity, Combinator, ComplexSelector, Component, LocalName}; -use parser::{Selector, SelectorInner, SelectorIter, SelectorImpl}; +use parser::{Combinator, ComplexSelector, Component, LocalName}; +use parser::{Selector, SelectorInner, SelectorIter}; use std::borrow::Borrow; use tree::Element; @@ -385,8 +387,8 @@ fn matches_simple_selector<E, F>( element.match_pseudo_element(pseudo, context) } Component::LocalName(LocalName { ref name, ref lower_name }) => { - let name = if element.is_html_element_in_html_document() { lower_name } else { name }; - element.get_local_name() == name.borrow() + let is_html = element.is_html_element_in_html_document(); + element.get_local_name() == select_name(is_html, name, lower_name).borrow() } Component::ExplicitUniversalType | Component::ExplicitAnyNamespace => { @@ -397,9 +399,8 @@ fn matches_simple_selector<E, F>( element.get_namespace() == url.borrow() } Component::ExplicitNoNamespace => { - // Rust type’s default, not default namespace - let empty_string = <E::Impl as SelectorImpl>::NamespaceUrl::default(); - element.get_namespace() == empty_string.borrow() + let ns = ::parser::namespace_empty_string::<E::Impl>(); + element.get_namespace() == ns.borrow() } // TODO: case-sensitivity depends on the document type and quirks mode Component::ID(ref id) => { @@ -409,35 +410,59 @@ fn matches_simple_selector<E, F>( Component::Class(ref class) => { element.has_class(class) } - Component::AttrExists(ref attr) => { - element.match_attr_has(attr) - } - Component::AttrEqual(ref attr, ref value, case_sensitivity) => { - match case_sensitivity { - CaseSensitivity::CaseSensitive => element.match_attr_equals(attr, value), - CaseSensitivity::CaseInsensitive => element.match_attr_equals_ignore_ascii_case(attr, value), + Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower } => { + let is_html = element.is_html_element_in_html_document(); + element.attr_matches( + &NamespaceConstraint::Specific(&::parser::namespace_empty_string::<E::Impl>()), + select_name(is_html, local_name, local_name_lower), + &AttrSelectorOperation::Exists + ) + } + Component::AttributeInNoNamespace { + ref local_name, + ref local_name_lower, + ref value, + operator, + case_sensitivity, + never_matches, + } => { + if never_matches { + return false } + let is_html = element.is_html_element_in_html_document(); + element.attr_matches( + &NamespaceConstraint::Specific(&::parser::namespace_empty_string::<E::Impl>()), + select_name(is_html, local_name, local_name_lower), + &AttrSelectorOperation::WithValue { + operator: operator, + case_sensitivity: case_sensitivity.to_unconditional(is_html), + expected_value: value, + } + ) } - Component::AttrIncludes(ref attr, ref value) => { - element.match_attr_includes(attr, value) - } - Component::AttrDashMatch(ref attr, ref value) => { - element.match_attr_dash(attr, value) - } - Component::AttrPrefixMatch(ref attr, ref value) => { - element.match_attr_prefix(attr, value) - } - Component::AttrSubstringMatch(ref attr, ref value) => { - element.match_attr_substring(attr, value) - } - Component::AttrSuffixMatch(ref attr, ref value) => { - element.match_attr_suffix(attr, value) - } - Component::AttrIncludesNeverMatch(..) | - Component::AttrPrefixNeverMatch(..) | - Component::AttrSubstringNeverMatch(..) | - Component::AttrSuffixNeverMatch(..) => { - false + Component::AttributeOther(ref attr_sel) => { + if attr_sel.never_matches { + return false + } + let is_html = element.is_html_element_in_html_document(); + element.attr_matches( + &attr_sel.namespace(), + select_name(is_html, &attr_sel.local_name, &attr_sel.local_name_lower), + &match attr_sel.operation { + ParsedAttrSelectorOperation::Exists => AttrSelectorOperation::Exists, + ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref expected_value, + } => { + AttrSelectorOperation::WithValue { + operator: operator, + case_sensitivity: case_sensitivity.to_unconditional(is_html), + expected_value: expected_value, + } + } + } + ) } Component::NonTSPseudoClass(ref pc) => { element.match_non_ts_pseudo_class(pc, context, flags_setter) @@ -489,6 +514,14 @@ fn matches_simple_selector<E, F>( } } +fn select_name<'a, T>(is_html: bool, local_name: &'a T, local_name_lower: &'a T) -> &'a T { + if is_html { + local_name_lower + } else { + local_name + } +} + #[inline] fn matches_generic_nth_child<E, F>(element: &E, a: i32, diff --git a/components/selectors/parser.rs b/components/selectors/parser.rs index 01f1eafc07c..197ce4f2629 100644 --- a/components/selectors/parser.rs +++ b/components/selectors/parser.rs @@ -3,6 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use arcslice::ArcSlice; +use attr::{AttrSelectorWithNamespace, ParsedAttrSelectorOperation, AttrSelectorOperator}; +use attr::{ParsedCaseSensitivity, SELECTOR_WHITESPACE, NamespaceConstraint}; use cssparser::{Token, Parser as CssParser, parse_nth, ToCss, serialize_identifier, CssStringWriter}; use precomputed_hash::PrecomputedHash; use smallvec::SmallVec; @@ -13,7 +15,6 @@ use std::fmt::{self, Display, Debug, Write}; use std::iter::Rev; use std::ops::Add; use std::slice; -use tree::SELECTOR_WHITESPACE; use visitor::SelectorVisitor; /// A trait that represents a pseudo-element. @@ -32,6 +33,16 @@ pub trait PseudoElement : Sized + ToCss { } } +fn to_ascii_lowercase(s: &str) -> Cow<str> { + if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') { + let mut string = s.to_owned(); + string[first_uppercase..].make_ascii_lowercase(); + string.into() + } else { + s.into() + } +} + macro_rules! with_all_bounds { ( [ $( $InSelector: tt )* ] @@ -45,16 +56,6 @@ macro_rules! with_all_bounds { } } - fn from_ascii_lowercase<T>(s: &str) -> T where T: $($FromStr)* { - if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') { - let mut string = s.to_owned(); - string[first_uppercase..].make_ascii_lowercase(); - T::from(string) - } else { - T::from(s) - } - } - /// This trait allows to define the parser implementation in regards /// of pseudo-classes/elements /// @@ -313,18 +314,37 @@ impl<Impl: SelectorImpl> SelectorMethods for Component<Impl> { return false; } } - }, - AttrExists(ref selector) | - AttrEqual(ref selector, _, _) | - AttrIncludes(ref selector, _) | - AttrDashMatch(ref selector, _) | - AttrPrefixMatch(ref selector, _) | - AttrSubstringMatch(ref selector, _) | - AttrSuffixMatch(ref selector, _) => { - if !visitor.visit_attribute_selector(selector) { + } + + AttributeInNoNamespaceExists { ref local_name, ref local_name_lower } => { + if !visitor.visit_attribute_selector( + &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()), + local_name, + local_name_lower, + ) { + return false; + } + } + AttributeInNoNamespace { ref local_name, ref local_name_lower, never_matches, .. } + if !never_matches => { + if !visitor.visit_attribute_selector( + &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()), + local_name, + local_name_lower, + ) { + return false; + } + } + AttributeOther(ref attr_selector) if !attr_selector.never_matches => { + if !visitor.visit_attribute_selector( + &attr_selector.namespace(), + &attr_selector.local_name, + &attr_selector.local_name_lower, + ) { return false; } } + NonTSPseudoClass(ref pseudo_class) => { if !pseudo_class.visit(visitor) { return false; @@ -337,6 +357,11 @@ impl<Impl: SelectorImpl> SelectorMethods for Component<Impl> { } } +pub fn namespace_empty_string<Impl: SelectorImpl>() -> Impl::NamespaceUrl { + // Rust type’s default, not default namespace + Impl::NamespaceUrl::default() +} + /// A ComplexSelectors stores a sequence of simple selectors and combinators. The /// iterator classes allow callers to iterate at either the raw sequence level or /// at the level of sequences of simple selectors separated by combinators. Most @@ -523,19 +548,20 @@ pub enum Component<Impl: SelectorImpl> { ID(Impl::Identifier), Class(Impl::ClassName), - // Attribute selectors - AttrExists(AttrSelector<Impl>), // [foo] - AttrEqual(AttrSelector<Impl>, Impl::AttrValue, CaseSensitivity), // [foo=bar] - AttrIncludes(AttrSelector<Impl>, Impl::AttrValue), // [foo~=bar] - AttrDashMatch(AttrSelector<Impl>, Impl::AttrValue), // [foo|=bar] - AttrPrefixMatch(AttrSelector<Impl>, Impl::AttrValue), // [foo^=bar] - AttrSubstringMatch(AttrSelector<Impl>, Impl::AttrValue), // [foo*=bar] - AttrSuffixMatch(AttrSelector<Impl>, Impl::AttrValue), // [foo$=bar] - - AttrIncludesNeverMatch(AttrSelector<Impl>, Impl::AttrValue), // empty value or with whitespace - AttrPrefixNeverMatch(AttrSelector<Impl>, Impl::AttrValue), // empty value - AttrSubstringNeverMatch(AttrSelector<Impl>, Impl::AttrValue), // empty value - AttrSuffixNeverMatch(AttrSelector<Impl>, Impl::AttrValue), // empty value + AttributeInNoNamespaceExists { + local_name: Impl::LocalName, + local_name_lower: Impl::LocalName, + }, + AttributeInNoNamespace { + local_name: Impl::LocalName, + local_name_lower: Impl::LocalName, + operator: AttrSelectorOperator, + value: Impl::AttrValue, + case_sensitivity: ParsedCaseSensitivity, + never_matches: bool, + }, + // Use a Box in the less common cases with more data to keep size_of::<Component>() small. + AttributeOther(Box<AttrSelectorWithNamespace<Impl>>), // Pseudo-classes // @@ -560,7 +586,6 @@ pub enum Component<Impl: SelectorImpl> { OnlyOfType, NonTSPseudoClass(Impl::NonTSPseudoClass), PseudoElement(Impl::PseudoElement), - // ... } impl<Impl: SelectorImpl> Component<Impl> { @@ -605,48 +630,12 @@ impl<Impl: SelectorImpl> Component<Impl> { } } -#[derive(Eq, PartialEq, Clone, Copy, Debug)] -pub enum CaseSensitivity { - CaseSensitive, // Selectors spec says language-defined, but HTML says sensitive. - CaseInsensitive, -} - - #[derive(Eq, PartialEq, Clone)] pub struct LocalName<Impl: SelectorImpl> { pub name: Impl::LocalName, pub lower_name: Impl::LocalName, } -#[derive(Eq, PartialEq, Clone)] -pub struct AttrSelector<Impl: SelectorImpl> { - pub name: Impl::LocalName, - pub lower_name: Impl::LocalName, - pub namespace: NamespaceConstraint<Impl>, -} - -#[derive(Eq, PartialEq, Clone, Debug)] -pub enum NamespaceConstraint<Impl: SelectorImpl> { - Any, - Specific(Namespace<Impl>), -} - -#[derive(Eq, PartialEq, Clone)] -pub struct Namespace<Impl: SelectorImpl> { - pub prefix: Option<Impl::NamespacePrefix>, - pub url: Impl::NamespaceUrl, -} - -impl<Impl: SelectorImpl> Default for Namespace<Impl> { - fn default() -> Self { - Namespace { - prefix: None, - url: Impl::NamespaceUrl::default(), // empty string - } - } -} - - impl<Impl: SelectorImpl> Debug for Selector<Impl> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("Selector(")?; @@ -664,10 +653,7 @@ impl<Impl: SelectorImpl> Debug for ComplexSelector<Impl> { impl<Impl: SelectorImpl> Debug for Component<Impl> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.to_css(f) } } -impl<Impl: SelectorImpl> Debug for AttrSelector<Impl> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.to_css(f) } -} -impl<Impl: SelectorImpl> Debug for Namespace<Impl> { +impl<Impl: SelectorImpl> Debug for AttrSelectorWithNamespace<Impl> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.to_css(f) } } impl<Impl: SelectorImpl> Debug for LocalName<Impl> { @@ -745,27 +731,26 @@ impl<Impl: SelectorImpl> ToCss for Component<Impl> { dest.write_char('|') } - // Attribute selectors - AttrExists(ref a) => { + AttributeInNoNamespaceExists { ref local_name, .. } => { dest.write_char('[')?; - a.to_css(dest)?; + display_to_css_identifier(local_name, dest)?; dest.write_char(']') } - AttrEqual(ref a, ref v, case) => { - attr_selector_to_css(a, " = ", v, match case { - CaseSensitivity::CaseSensitive => None, - CaseSensitivity::CaseInsensitive => Some(" i"), - }, dest) + AttributeInNoNamespace { ref local_name, operator, ref value, case_sensitivity, .. } => { + dest.write_char('[')?; + display_to_css_identifier(local_name, dest)?; + operator.to_css(dest)?; + dest.write_char('"')?; + write!(CssStringWriter::new(dest), "{}", value)?; + dest.write_char('"')?; + match case_sensitivity { + ParsedCaseSensitivity::CaseSensitive | + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}, + ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + } + dest.write_char(']') } - AttrDashMatch(ref a, ref v) => attr_selector_to_css(a, " |= ", v, None, dest), - AttrIncludesNeverMatch(ref a, ref v) | - AttrIncludes(ref a, ref v) => attr_selector_to_css(a, " ~= ", v, None, dest), - AttrPrefixNeverMatch(ref a, ref v) | - AttrPrefixMatch(ref a, ref v) => attr_selector_to_css(a, " ^= ", v, None, dest), - AttrSubstringNeverMatch(ref a, ref v) | - AttrSubstringMatch(ref a, ref v) => attr_selector_to_css(a, " *= ", v, None, dest), - AttrSuffixNeverMatch(ref a, ref v) | - AttrSuffixMatch(ref a, ref v) => attr_selector_to_css(a, " $= ", v, None, dest), + AttributeOther(ref attr_selector) => attr_selector.to_css(dest), // Pseudo-classes Negation(ref arg) => { @@ -794,22 +779,36 @@ impl<Impl: SelectorImpl> ToCss for Component<Impl> { } } -impl<Impl: SelectorImpl> ToCss for AttrSelector<Impl> { +impl<Impl: SelectorImpl> ToCss for AttrSelectorWithNamespace<Impl> { fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { - if let NamespaceConstraint::Specific(ref ns) = self.namespace { - ns.to_css(dest)?; + dest.write_char('[')?; + match self.namespace { + NamespaceConstraint::Specific((ref prefix, _)) => { + display_to_css_identifier(prefix, dest)?; + dest.write_char('|')? + } + NamespaceConstraint::Any => { + dest.write_str("*|")? + } } - display_to_css_identifier(&self.name, dest) - } -} - -impl<Impl: SelectorImpl> ToCss for Namespace<Impl> { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { - if let Some(ref prefix) = self.prefix { - display_to_css_identifier(prefix, dest)?; - dest.write_char('|')?; + display_to_css_identifier(&self.local_name, dest)?; + match self.operation { + ParsedAttrSelectorOperation::Exists => {}, + ParsedAttrSelectorOperation::WithValue { + operator, case_sensitivity, ref expected_value + } => { + operator.to_css(dest)?; + dest.write_char('"')?; + write!(CssStringWriter::new(dest), "{}", expected_value)?; + dest.write_char('"')?; + match case_sensitivity { + ParsedCaseSensitivity::CaseSensitive | + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}, + ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + } + }, } - Ok(()) + dest.write_char(']') } } @@ -819,26 +818,6 @@ impl<Impl: SelectorImpl> ToCss for LocalName<Impl> { } } -fn attr_selector_to_css<Impl, W>(attr: &AttrSelector<Impl>, - operator: &str, - value: &Impl::AttrValue, - modifier: Option<&str>, - dest: &mut W) - -> fmt::Result -where Impl: SelectorImpl, W: fmt::Write -{ - dest.write_char('[')?; - attr.to_css(dest)?; - dest.write_str(operator)?; - dest.write_char('"')?; - write!(CssStringWriter::new(dest), "{}", value)?; - dest.write_char('"')?; - if let Some(m) = modifier { - dest.write_str(m)?; - } - dest.write_char(']') -} - /// Serialize the output of Display as a CSS identifier fn display_to_css_identifier<T: Display, W: fmt::Write>(x: &T, dest: &mut W) -> fmt::Result { // FIXME(SimonSapin): it is possible to avoid this heap allocation @@ -934,18 +913,9 @@ fn complex_selector_specificity<Impl>(selector: &ComplexSelector<Impl>) specificity.id_selectors += 1 } Component::Class(..) | - Component::AttrExists(..) | - Component::AttrEqual(..) | - Component::AttrIncludes(..) | - Component::AttrDashMatch(..) | - Component::AttrPrefixMatch(..) | - Component::AttrSubstringMatch(..) | - Component::AttrSuffixMatch(..) | - - Component::AttrIncludesNeverMatch(..) | - Component::AttrPrefixNeverMatch(..) | - Component::AttrSubstringNeverMatch(..) | - Component::AttrSuffixNeverMatch(..) | + Component::AttributeInNoNamespace { .. } | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeOther(..) | Component::FirstChild | Component::LastChild | Component::OnlyChild | Component::Root | @@ -1121,7 +1091,7 @@ fn parse_type_selector<P, Impl>(parser: &P, input: &mut CssParser, sequence: &mu match local_name { Some(name) => { sequence.push(Component::LocalName(LocalName { - lower_name: from_ascii_lowercase(&name), + lower_name: from_cow_str(to_ascii_lowercase(&name)), name: from_cow_str(name), })) } @@ -1231,98 +1201,143 @@ fn parse_attribute_selector<P, Impl>(parser: &P, input: &mut CssParser) -> Result<Component<Impl>, ()> where P: Parser<Impl=Impl>, Impl: SelectorImpl { - let attr = match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? { + let namespace; + let local_name; + match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? { None => return Err(()), Some((_, None)) => unreachable!(), - Some((namespace, Some(local_name))) => AttrSelector { - namespace: match namespace { + Some((ns, Some(ln))) => { + local_name = ln; + namespace = match ns { QNamePrefix::ImplicitNoNamespace | QNamePrefix::ExplicitNoNamespace => { - NamespaceConstraint::Specific(Namespace { - prefix: None, - url: Impl::NamespaceUrl::default(), - }) + None } QNamePrefix::ExplicitNamespace(prefix, url) => { - NamespaceConstraint::Specific(Namespace { - prefix: Some(prefix), - url: url, - }) + Some(NamespaceConstraint::Specific((prefix, url))) } QNamePrefix::ExplicitAnyNamespace => { - NamespaceConstraint::Any + Some(NamespaceConstraint::Any) } QNamePrefix::ImplicitAnyNamespace | QNamePrefix::ImplicitDefaultNamespace(_) => { unreachable!() // Not returned with in_attr_selector = true } - }, - lower_name: from_ascii_lowercase(&local_name), - name: from_cow_str(local_name), - }, - }; + } + } + } + let operator; + let value; + let never_matches; match input.next() { // [foo] - Err(()) => Ok(Component::AttrExists(attr)), + Err(()) => { + let local_name_lower = from_cow_str(to_ascii_lowercase(&local_name)); + let local_name = from_cow_str(local_name); + if let Some(namespace) = namespace { + return Ok(Component::AttributeOther(Box::new(AttrSelectorWithNamespace { + namespace: namespace, + local_name: local_name, + local_name_lower: local_name_lower, + operation: ParsedAttrSelectorOperation::Exists, + never_matches: false, + }))) + } else { + return Ok(Component::AttributeInNoNamespaceExists { + local_name: local_name, + local_name_lower: local_name_lower, + }) + } + } // [foo=bar] Ok(Token::Delim('=')) => { - let value = input.expect_ident_or_string()?; - let flags = parse_attribute_flags(input)?; - Ok(Component::AttrEqual(attr, from_cow_str(value), flags)) + value = input.expect_ident_or_string()?; + never_matches = false; + operator = AttrSelectorOperator::Equal; } // [foo~=bar] Ok(Token::IncludeMatch) => { - let value = input.expect_ident_or_string()?; - if value.is_empty() || value.contains(SELECTOR_WHITESPACE) { - Ok(Component::AttrIncludesNeverMatch(attr, from_cow_str(value))) - } else { - Ok(Component::AttrIncludes(attr, from_cow_str(value))) - } + value = input.expect_ident_or_string()?; + never_matches = value.is_empty() || value.contains(SELECTOR_WHITESPACE); + operator = AttrSelectorOperator::Includes; } // [foo|=bar] Ok(Token::DashMatch) => { - let value = input.expect_ident_or_string()?; - Ok(Component::AttrDashMatch(attr, from_cow_str(value))) + value = input.expect_ident_or_string()?; + never_matches = false; + operator = AttrSelectorOperator::DashMatch; } // [foo^=bar] Ok(Token::PrefixMatch) => { - let value = input.expect_ident_or_string()?; - if value.is_empty() { - Ok(Component::AttrPrefixNeverMatch(attr, from_cow_str(value))) - } else { - Ok(Component::AttrPrefixMatch(attr, from_cow_str(value))) - } + value = input.expect_ident_or_string()?; + never_matches = value.is_empty(); + operator = AttrSelectorOperator::Prefix; } // [foo*=bar] Ok(Token::SubstringMatch) => { - let value = input.expect_ident_or_string()?; - if value.is_empty() { - Ok(Component::AttrSubstringNeverMatch(attr, from_cow_str(value))) - } else { - Ok(Component::AttrSubstringMatch(attr, from_cow_str(value))) - } + value = input.expect_ident_or_string()?; + never_matches = value.is_empty(); + operator = AttrSelectorOperator::Substring; } // [foo$=bar] Ok(Token::SuffixMatch) => { - let value = input.expect_ident_or_string()?; - if value.is_empty() { - Ok(Component::AttrSuffixNeverMatch(attr, from_cow_str(value))) - } else { - Ok(Component::AttrSuffixMatch(attr, from_cow_str(value))) + value = input.expect_ident_or_string()?; + never_matches = value.is_empty(); + operator = AttrSelectorOperator::Suffix; + } + _ => return Err(()) + } + + let mut case_sensitivity = parse_attribute_flags(input)?; + + let value = from_cow_str(value); + let local_name_lower; + { + let local_name_lower_cow = to_ascii_lowercase(&local_name); + if let ParsedCaseSensitivity::CaseSensitive = case_sensitivity { + if namespace.is_none() && + include!(concat!(env!("OUT_DIR"), "/ascii_case_insensitive_html_attributes.rs")) + .contains(&*local_name_lower_cow) + { + case_sensitivity = + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument } } - _ => Err(()) + local_name_lower = from_cow_str(local_name_lower_cow); + } + let local_name = from_cow_str(local_name); + if let Some(namespace) = namespace { + Ok(Component::AttributeOther(Box::new(AttrSelectorWithNamespace { + namespace: namespace, + local_name: local_name, + local_name_lower: local_name_lower, + never_matches: never_matches, + operation: ParsedAttrSelectorOperation::WithValue { + operator: operator, + case_sensitivity: case_sensitivity, + expected_value: value, + } + }))) + } else { + Ok(Component::AttributeInNoNamespace { + local_name: local_name, + local_name_lower: local_name_lower, + operator: operator, + value: value, + case_sensitivity: case_sensitivity, + never_matches: never_matches, + }) } } -fn parse_attribute_flags(input: &mut CssParser) -> Result<CaseSensitivity, ()> { +fn parse_attribute_flags(input: &mut CssParser) -> Result<ParsedCaseSensitivity, ()> { match input.next() { - Err(()) => Ok(CaseSensitivity::CaseSensitive), + Err(()) => Ok(ParsedCaseSensitivity::CaseSensitive), Ok(Token::Ident(ref value)) if value.eq_ignore_ascii_case("i") => { - Ok(CaseSensitivity::CaseInsensitive) + Ok(ParsedCaseSensitivity::AsciiCaseInsensitive) } _ => Err(()) } @@ -1853,14 +1868,12 @@ pub mod tests { // https://github.com/mozilla/servo/pull/1652 let mut parser = DummyParser::default(); assert_eq!(parse_ns("[Foo]", &parser), Ok(SelectorList(vec!(Selector { - inner: SelectorInner::from_vec(vec!( - Component::AttrExists(AttrSelector { - name: DummyAtom::from("Foo"), - lower_name: DummyAtom::from("foo"), - namespace: NamespaceConstraint::Specific(Namespace { - prefix: None, - url: "".into(), - }) }))), + inner: SelectorInner::from_vec(vec![ + Component::AttributeInNoNamespaceExists { + local_name: DummyAtom::from("Foo"), + local_name_lower: DummyAtom::from("foo"), + } + ]), specificity_and_flags: specificity(0, 1, 0), })))); assert_eq!(parse_ns("svg|circle", &parser), Err(())); @@ -1893,14 +1906,10 @@ pub mod tests { inner: SelectorInner::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), - Component::AttrExists(AttrSelector { - name: DummyAtom::from("Foo"), - lower_name: DummyAtom::from("foo"), - namespace: NamespaceConstraint::Specific(Namespace { - prefix: None, - url: "".into(), - }), - }), + Component::AttributeInNoNamespaceExists { + local_name: DummyAtom::from("Foo"), + local_name_lower: DummyAtom::from("foo"), + }, ]), specificity_and_flags: specificity(0, 1, 0), })))); @@ -1968,14 +1977,14 @@ pub mod tests { assert_eq!(parse("[attr |= \"foo\"]"), Ok(SelectorList(vec![Selector { inner: SelectorInner::from_vec( vec![ - Component::AttrDashMatch(AttrSelector { - name: DummyAtom::from("attr"), - lower_name: DummyAtom::from("attr"), - namespace: NamespaceConstraint::Specific(Namespace { - prefix: None, - url: "".into(), - }), - }, DummyAtom::from("foo")) + Component::AttributeInNoNamespace { + local_name: DummyAtom::from("attr"), + local_name_lower: DummyAtom::from("attr"), + operator: AttrSelectorOperator::DashMatch, + value: DummyAtom::from("foo"), + never_matches: false, + case_sensitivity: ParsedCaseSensitivity::CaseSensitive, + } ]), specificity_and_flags: specificity(0, 1, 0), }]))); diff --git a/components/selectors/size_of_tests.rs b/components/selectors/size_of_tests.rs index c03a0c80db5..5fec3085fcb 100644 --- a/components/selectors/size_of_tests.rs +++ b/components/selectors/size_of_tests.rs @@ -16,8 +16,7 @@ size_of_test!(size_of_pseudo_element, gecko_like_types::PseudoElement, 1); size_of_test!(size_of_selector_inner, SelectorInner<Impl>, 40); size_of_test!(size_of_complex_selector, ComplexSelector<Impl>, 24); -size_of_test!(size_of_component, Component<Impl>, 64); -size_of_test!(size_of_attr_selector, AttrSelector<Impl>, 48); +size_of_test!(size_of_component, Component<Impl>, 32); size_of_test!(size_of_pseudo_class, PseudoClass, 24); impl parser::PseudoElement for gecko_like_types::PseudoElement { diff --git a/components/selectors/tree.rs b/components/selectors/tree.rs index e5a4539e3cc..276c788d05b 100644 --- a/components/selectors/tree.rs +++ b/components/selectors/tree.rs @@ -5,122 +5,13 @@ //! Traits that nodes must implement. Breaks the otherwise-cyclic dependency between layout and //! style. +use attr::{AttrSelectorOperation, NamespaceConstraint}; use matching::{ElementSelectorFlags, MatchingContext}; -use parser::{AttrSelector, SelectorImpl}; -use std::ascii::AsciiExt; +use parser::SelectorImpl; -/// The definition of whitespace per CSS Selectors Level 3 § 4. -pub static SELECTOR_WHITESPACE: &'static [char] = &[' ', '\t', '\n', '\r', '\x0C']; - -// Attribute matching routines. Consumers with simple implementations can implement -// MatchAttrGeneric instead. -pub trait MatchAttr { - type Impl: SelectorImpl; - - fn match_attr_has( - &self, - attr: &AttrSelector<Self::Impl>) -> bool; - - fn match_attr_equals( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; - - fn match_attr_equals_ignore_ascii_case( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; - - fn match_attr_includes( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; - - fn match_attr_dash( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; - - fn match_attr_prefix( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; - - fn match_attr_substring( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; - - fn match_attr_suffix( - &self, - attr: &AttrSelector<Self::Impl>, - value: &<Self::Impl as SelectorImpl>::AttrValue) -> bool; -} - -pub trait MatchAttrGeneric { +pub trait Element: Sized { type Impl: SelectorImpl; - fn match_attr<F>(&self, attr: &AttrSelector<Self::Impl>, test: F) -> bool where F: Fn(&str) -> bool; -} - -impl<T> MatchAttr for T where T: MatchAttrGeneric, T::Impl: SelectorImpl<AttrValue = String> { - type Impl = T::Impl; - - fn match_attr_has(&self, attr: &AttrSelector<Self::Impl>) -> bool { - self.match_attr(attr, |_| true) - } - - fn match_attr_equals(&self, attr: &AttrSelector<Self::Impl>, value: &String) -> bool { - self.match_attr(attr, |v| v == value) - } - - fn match_attr_equals_ignore_ascii_case(&self, attr: &AttrSelector<Self::Impl>, - value: &String) -> bool { - self.match_attr(attr, |v| v.eq_ignore_ascii_case(value)) - } - - fn match_attr_includes(&self, attr: &AttrSelector<Self::Impl>, value: &String) -> bool { - self.match_attr(attr, |attr_value| { - attr_value.split(SELECTOR_WHITESPACE).any(|v| v == value) - }) - } - fn match_attr_dash(&self, attr: &AttrSelector<Self::Impl>, value: &String) -> bool { - self.match_attr(attr, |attr_value| { - // The attribute must start with the pattern. - if !attr_value.starts_with(value) { - return false - } - - // If the strings are the same, we're done. - if attr_value.len() == value.len() { - return true - } - - // The attribute is long than the pattern, so the next character must be '-'. - attr_value.as_bytes()[value.len()] == '-' as u8 - }) - } - - fn match_attr_prefix(&self, attr: &AttrSelector<Self::Impl>, value: &String) -> bool { - self.match_attr(attr, |attr_value| { - attr_value.starts_with(value) - }) - } - - fn match_attr_substring(&self, attr: &AttrSelector<Self::Impl>, value: &String) -> bool { - self.match_attr(attr, |attr_value| { - attr_value.contains(value) - }) - } - - fn match_attr_suffix(&self, attr: &AttrSelector<Self::Impl>, value: &String) -> bool { - self.match_attr(attr, |attr_value| { - attr_value.ends_with(value) - }) - } -} - -pub trait Element: MatchAttr + Sized { fn parent_element(&self) -> Option<Self>; /// The parent of a given pseudo-element, after matching a pseudo-element @@ -131,22 +22,31 @@ pub trait Element: MatchAttr + Sized { self.parent_element() } - // Skips non-element nodes + /// Skips non-element nodes fn first_child_element(&self) -> Option<Self>; - // Skips non-element nodes + /// Skips non-element nodes fn last_child_element(&self) -> Option<Self>; - // Skips non-element nodes + /// Skips non-element nodes fn prev_sibling_element(&self) -> Option<Self>; - // Skips non-element nodes + /// Skips non-element nodes fn next_sibling_element(&self) -> Option<Self>; fn is_html_element_in_html_document(&self) -> bool; + fn get_local_name(&self) -> &<Self::Impl as SelectorImpl>::BorrowedLocalName; + + /// Empty string for no namespace fn get_namespace(&self) -> &<Self::Impl as SelectorImpl>::BorrowedNamespaceUrl; + fn attr_matches(&self, + ns: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>, + local_name: &<Self::Impl as SelectorImpl>::LocalName, + operation: &AttrSelectorOperation<&<Self::Impl as SelectorImpl>::AttrValue>) + -> bool; + fn match_non_ts_pseudo_class<F>(&self, pc: &<Self::Impl as SelectorImpl>::NonTSPseudoClass, context: &mut MatchingContext, @@ -159,6 +59,7 @@ pub trait Element: MatchAttr + Sized { -> bool; fn get_id(&self) -> Option<<Self::Impl as SelectorImpl>::Identifier>; + fn has_class(&self, name: &<Self::Impl as SelectorImpl>::ClassName) -> bool; /// Returns whether this element matches `:empty`. @@ -173,10 +74,4 @@ pub trait Element: MatchAttr + Sized { /// Note: this can be false even if `.parent_element()` is `None` /// if the parent node is a `DocumentFragment`. fn is_root(&self) -> bool; - - // Ordinarily I wouldn't use callbacks like this, but the alternative is - // really messy, since there is a `JSRef` and a `RefCell` involved. Maybe - // in the future when we have associated types and/or a more convenient - // JS GC story... --pcwalton - fn each_class<F>(&self, callback: F) where F: FnMut(&<Self::Impl as SelectorImpl>::ClassName); } diff --git a/components/selectors/visitor.rs b/components/selectors/visitor.rs index be335aed87b..07f121fe82f 100644 --- a/components/selectors/visitor.rs +++ b/components/selectors/visitor.rs @@ -6,8 +6,8 @@ #![deny(missing_docs)] -use parser::{AttrSelector, Combinator, Component}; -use parser::{SelectorImpl, SelectorIter}; +use attr::NamespaceConstraint; +use parser::{Combinator, Component, SelectorImpl, SelectorIter}; /// A trait to visit selector properties. /// @@ -20,7 +20,12 @@ pub trait SelectorVisitor { /// Visit an attribute selector that may match (there are other selectors /// that may never match, like those containing whitespace or the empty /// string). - fn visit_attribute_selector(&mut self, _: &AttrSelector<Self::Impl>) -> bool { + fn visit_attribute_selector( + &mut self, + _namespace: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>, + _local_name: &<Self::Impl as SelectorImpl>::LocalName, + _local_name_lower: &<Self::Impl as SelectorImpl>::LocalName, + ) -> bool { true } |