From b22e0e94942c2de3d215033a06695f53d69cddbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez=20Moreno?= Date: Tue, 9 May 2017 10:41:03 +0200 Subject: [PATCH] Stylo: Bug 1355408 - add support for @-moz-document --- Cargo.lock | 1 + components/script/dom/cssrule.rs | 1 + components/style/build_gecko.rs | 2 + components/style/document_condition.rs | 176 ++++++++++++++++++ components/style/gecko/generated/bindings.rs | 9 + .../style/gecko/generated/structs_debug.rs | 12 ++ .../style/gecko/generated/structs_release.rs | 12 ++ components/style/lib.rs | 1 + components/style/stylesheets.rs | 107 +++++++++-- components/style/stylist.rs | 25 ++- ports/geckolib/Cargo.toml | 1 + tests/unit/style/media_queries.rs | 15 +- 12 files changed, 338 insertions(+), 24 deletions(-) create mode 100644 components/style/document_condition.rs diff --git a/Cargo.lock b/Cargo.lock index fb927c48ffe3..13c3ea29af0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,6 +963,7 @@ dependencies = [ "env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "nsstring_vendor 0.1.0", "parking_lot 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "selectors 0.18.0", "style 0.0.1", diff --git a/components/script/dom/cssrule.rs b/components/script/dom/cssrule.rs index 6bfe057d6d9c..32e6ad05471b 100644 --- a/components/script/dom/cssrule.rs +++ b/components/script/dom/cssrule.rs @@ -85,6 +85,7 @@ impl CSSRule { StyleCssRule::Viewport(s) => Root::upcast(CSSViewportRule::new(window, parent_stylesheet, s)), StyleCssRule::Supports(s) => Root::upcast(CSSSupportsRule::new(window, parent_stylesheet, s)), StyleCssRule::Page(_) => unreachable!(), + StyleCssRule::Document(_) => unimplemented!(), // TODO } } diff --git a/components/style/build_gecko.rs b/components/style/build_gecko.rs index af8d1c722520..3b80bd338191 100644 --- a/components/style/build_gecko.rs +++ b/components/style/build_gecko.rs @@ -349,6 +349,7 @@ mod bindings { "mozilla::ServoElementSnapshot.*", "mozilla::CSSPseudoClassType", "mozilla::css::SheetParsingMode", + "mozilla::css::URLMatchingFunction", "mozilla::HalfCorner", "mozilla::PropertyStyleAnimationValuePair", "mozilla::TraversalRestyleBehavior", @@ -739,6 +740,7 @@ mod bindings { "UpdateAnimationsTasks", "LengthParsingMode", "InheritTarget", + "URLMatchingFunction", ]; struct ArrayType { cpp_type: &'static str, diff --git a/components/style/document_condition.rs b/components/style/document_condition.rs new file mode 100644 index 000000000000..aef034f2eebd --- /dev/null +++ b/components/style/document_condition.rs @@ -0,0 +1,176 @@ +/* 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/. */ + +//! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) +//! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4. +//! We implement the prefixed `@-moz-document`. + +use cssparser::{Parser, Token, serialize_string}; +#[cfg(feature = "gecko")] +use gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation; +#[cfg(feature = "gecko")] +use gecko_bindings::structs::URLMatchingFunction as GeckoUrlMatchingFunction; +use media_queries::Device; +#[cfg(feature = "gecko")] +use nsstring::nsCString; +use parser::{Parse, ParserContext}; +use std::fmt; +use style_traits::ToCss; +use values::specified::url::SpecifiedUrl; + +/// A URL matching function for a `@document` rule's condition. +#[derive(Debug)] +pub enum UrlMatchingFunction { + /// Exact URL matching function. It evaluates to true whenever the + /// URL of the document being styled is exactly the URL given. + Url(SpecifiedUrl), + /// URL prefix matching function. It evaluates to true whenever the + /// URL of the document being styled has the argument to the + /// function as an initial substring (which is true when the two + /// strings are equal). When the argument is the empty string, + /// it evaluates to true for all documents. + UrlPrefix(String), + /// Domain matching function. It evaluates to true whenever the URL + /// of the document being styled has a host subcomponent and that + /// host subcomponent is exactly the argument to the ‘domain()’ + /// function or a final substring of the host component is a + /// period (U+002E) immediately followed by the argument to the + /// ‘domain()’ function. + Domain(String), + /// Regular expression matching function. It evaluates to true + /// whenever the regular expression matches the entirety of the URL + /// of the document being styled. + RegExp(String), +} + +macro_rules! parse_quoted_or_unquoted_string { + ($input:ident, $url_matching_function:expr) => { + $input.parse_nested_block(|input| { + let start = input.position(); + input.parse_entirely(|input| { + match input.next() { + Ok(Token::QuotedString(value)) => + Ok($url_matching_function(value.into_owned())), + _ => Err(()), + } + }).or_else(|_| { + while let Ok(_) = input.next() {} + Ok($url_matching_function(input.slice_from(start).to_string())) + }) + }) + } +} + +impl UrlMatchingFunction { + /// Parse a URL matching function for a`@document` rule's condition. + pub fn parse(context: &ParserContext, input: &mut Parser) + -> Result { + if input.try(|input| input.expect_function_matching("url-prefix")).is_ok() { + parse_quoted_or_unquoted_string!(input, UrlMatchingFunction::UrlPrefix) + } else if input.try(|input| input.expect_function_matching("domain")).is_ok() { + parse_quoted_or_unquoted_string!(input, UrlMatchingFunction::Domain) + } else if input.try(|input| input.expect_function_matching("regexp")).is_ok() { + input.parse_nested_block(|input| { + Ok(UrlMatchingFunction::RegExp(input.expect_string()?.into_owned())) + }) + } else if let Ok(url) = input.try(|input| SpecifiedUrl::parse(context, input)) { + Ok(UrlMatchingFunction::Url(url)) + } else { + Err(()) + } + } + + #[cfg(feature = "gecko")] + /// Evaluate a URL matching function. + pub fn evaluate(&self, device: &Device) -> bool { + let func = match *self { + UrlMatchingFunction::Url(_) => GeckoUrlMatchingFunction::eURL, + UrlMatchingFunction::UrlPrefix(_) => GeckoUrlMatchingFunction::eURLPrefix, + UrlMatchingFunction::Domain(_) => GeckoUrlMatchingFunction::eDomain, + UrlMatchingFunction::RegExp(_) => GeckoUrlMatchingFunction::eRegExp, + }; + let pattern = nsCString::from(match *self { + UrlMatchingFunction::Url(ref url) => url.as_str(), + UrlMatchingFunction::UrlPrefix(ref pat) | + UrlMatchingFunction::Domain(ref pat) | + UrlMatchingFunction::RegExp(ref pat) => pat, + }); + unsafe { + Gecko_DocumentRule_UseForPresentation(&*device.pres_context, &*pattern, func) + } + } + + #[cfg(not(feature = "gecko"))] + /// Evaluate a URL matching function. + pub fn evaluate(&self, _: &Device) -> bool { + false + } +} + +impl ToCss for UrlMatchingFunction { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write { + match *self { + UrlMatchingFunction::Url(ref url) => { + url.to_css(dest) + }, + UrlMatchingFunction::UrlPrefix(ref url_prefix) => { + dest.write_str("url-prefix(")?; + serialize_string(url_prefix, dest)?; + dest.write_str(")") + }, + UrlMatchingFunction::Domain(ref domain) => { + dest.write_str("domain(")?; + serialize_string(domain, dest)?; + dest.write_str(")") + }, + UrlMatchingFunction::RegExp(ref regex) => { + dest.write_str("regexp(")?; + serialize_string(regex, dest)?; + dest.write_str(")") + }, + } + } +} + +/// A `@document` rule's condition. +/// +/// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document +/// +/// The `@document` rule's condition is written as a comma-separated list of +/// URL matching functions, and the condition evaluates to true whenever any +/// one of those functions evaluates to true. +#[derive(Debug)] +pub struct DocumentCondition(Vec); + +impl DocumentCondition { + /// Parse a document condition. + pub fn parse(context: &ParserContext, input: &mut Parser) + -> Result { + input.parse_comma_separated(|input| UrlMatchingFunction::parse(context, input)) + .map(DocumentCondition) + } + + /// Evaluate a document condition. + pub fn evaluate(&self, device: &Device) -> bool { + self.0.iter().any(|ref url_matching_function| + url_matching_function.evaluate(device) + ) + } +} + +impl ToCss for DocumentCondition { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write { + let mut iter = self.0.iter(); + let first = iter.next() + .expect("Empty DocumentCondition, should contain at least one URL matching function"); + first.to_css(dest)?; + for url_matching_function in iter { + dest.write_str(", ")?; + url_matching_function.to_css(dest)?; + } + Ok(()) + } +} diff --git a/components/style/gecko/generated/bindings.rs b/components/style/gecko/generated/bindings.rs index d068add23753..fdd7d7c7ac81 100644 --- a/components/style/gecko/generated/bindings.rs +++ b/components/style/gecko/generated/bindings.rs @@ -194,6 +194,7 @@ use gecko_bindings::structs::EffectCompositor_CascadeLevel; use gecko_bindings::structs::UpdateAnimationsTasks; use gecko_bindings::structs::LengthParsingMode; use gecko_bindings::structs::InheritTarget; +use gecko_bindings::structs::URLMatchingFunction; pub type nsTArrayBorrowed_uintptr_t<'a> = &'a mut ::gecko_bindings::structs::nsTArray; pub type ServoCssRulesStrong = ::gecko_bindings::sugar::ownership::Strong; pub type ServoCssRulesBorrowed<'a> = &'a ServoCssRules; @@ -1549,6 +1550,14 @@ extern "C" { extern "C" { pub fn Gecko_UnregisterProfilerThread(); } +extern "C" { + pub fn Gecko_DocumentRule_UseForPresentation(arg1: + RawGeckoPresContextBorrowed, + aPattern: *const nsACString, + aURLMatchingFunction: + URLMatchingFunction) + -> bool; +} extern "C" { pub fn Servo_Element_ClearData(node: RawGeckoElementBorrowed); } diff --git a/components/style/gecko/generated/structs_debug.rs b/components/style/gecko/generated/structs_debug.rs index dc5734d3a214..7ac6548e1fd5 100644 --- a/components/style/gecko/generated/structs_debug.rs +++ b/components/style/gecko/generated/structs_debug.rs @@ -2130,6 +2130,18 @@ pub mod root { ComplexColorValue ) , "::" , stringify ! ( _mOwningThread ) )); } + #[repr(i32)] + /** + * Enum defining the type of URL matching function for a @-moz-document rule + * condition. + */ + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] + pub enum URLMatchingFunction { + eURL = 0, + eURLPrefix = 1, + eDomain = 2, + eRegExp = 3, + } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct DocumentRule { diff --git a/components/style/gecko/generated/structs_release.rs b/components/style/gecko/generated/structs_release.rs index 035c42bbf8a1..6abecf98e099 100644 --- a/components/style/gecko/generated/structs_release.rs +++ b/components/style/gecko/generated/structs_release.rs @@ -2036,6 +2036,18 @@ pub mod root { ComplexColorValue ) , "::" , stringify ! ( mRefCnt ) )); } + #[repr(i32)] + /** + * Enum defining the type of URL matching function for a @-moz-document rule + * condition. + */ + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] + pub enum URLMatchingFunction { + eURL = 0, + eURLPrefix = 1, + eDomain = 2, + eRegExp = 3, + } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct DocumentRule { diff --git a/components/style/lib.rs b/components/style/lib.rs index 8a21e15d6483..91c8727a2e7b 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -93,6 +93,7 @@ pub mod context; pub mod counter_style; pub mod custom_properties; pub mod data; +pub mod document_condition; pub mod dom; pub mod element_state; #[cfg(feature = "servo")] mod encoding_support; diff --git a/components/style/stylesheets.rs b/components/style/stylesheets.rs index 36e5a2a936e0..5b19e9a14d35 100644 --- a/components/style/stylesheets.rs +++ b/components/style/stylesheets.rs @@ -12,6 +12,7 @@ use counter_style::{CounterStyleRule, parse_counter_style_name, parse_counter_st use cssparser::{AtRuleParser, Parser, QualifiedRuleParser}; use cssparser::{AtRuleType, RuleListParser, parse_one_rule}; use cssparser::ToCss as ParserToCss; +use document_condition::DocumentCondition; use error_reporting::{ParseErrorReporter, NullReporter}; #[cfg(feature = "servo")] use font_face::FontFaceRuleData; @@ -299,6 +300,7 @@ pub enum CssRule { Keyframes(Arc>), Supports(Arc>), Page(Arc>), + Document(Arc>), } #[allow(missing_docs)] @@ -321,12 +323,24 @@ pub enum CssRuleType { CounterStyle = 11, // https://drafts.csswg.org/css-conditional-3/#extentions-to-cssrule-interface Supports = 12, + // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#extentions-to-cssrule-interface + Document = 13, // https://drafts.csswg.org/css-fonts-3/#om-fontfeaturevalues FontFeatureValues = 14, // https://drafts.csswg.org/css-device-adapt/#css-rule-interface Viewport = 15, } +/// Result type for with_nested_rules_mq_and_doc_rule() +pub enum NestedRulesResult<'a> { + /// Only rules + Rules(&'a [CssRule]), + /// Rules with media queries + RulesWithMediaQueries(&'a [CssRule], &'a MediaList), + /// Rules with document rule + RulesWithDocument(&'a [CssRule], &'a DocumentRule) +} + #[allow(missing_docs)] pub enum SingleRuleParseError { Syntax, @@ -347,6 +361,7 @@ impl CssRule { CssRule::Viewport(_) => CssRuleType::Viewport, CssRule::Supports(_) => CssRuleType::Supports, CssRule::Page(_) => CssRuleType::Page, + CssRule::Document(_) => CssRuleType::Document, } } @@ -365,8 +380,8 @@ impl CssRule { /// used for others. /// /// This will not recurse down unsupported @supports rules - pub fn with_nested_rules_and_mq(&self, guard: &SharedRwLockReadGuard, mut f: F) -> R - where F: FnMut(&[CssRule], Option<&MediaList>) -> R { + pub fn with_nested_rules_mq_and_doc_rule(&self, guard: &SharedRwLockReadGuard, mut f: F) -> R + where F: FnMut(NestedRulesResult) -> R { match *self { CssRule::Import(ref lock) => { let rule = lock.read_with(guard); @@ -374,7 +389,7 @@ impl CssRule { let rules = rule.stylesheet.rules.read_with(guard); // FIXME(emilio): Include the nested rules if the stylesheet is // loaded. - f(&rules.0, Some(&media)) + f(NestedRulesResult::RulesWithMediaQueries(&rules.0, &media)) } CssRule::Namespace(_) | CssRule::Style(_) | @@ -383,22 +398,31 @@ impl CssRule { CssRule::Viewport(_) | CssRule::Keyframes(_) | CssRule::Page(_) => { - f(&[], None) + f(NestedRulesResult::Rules(&[])) } CssRule::Media(ref lock) => { let media_rule = lock.read_with(guard); let mq = media_rule.media_queries.read_with(guard); let rules = &media_rule.rules.read_with(guard).0; - f(rules, Some(&mq)) + f(NestedRulesResult::RulesWithMediaQueries(rules, &mq)) } CssRule::Supports(ref lock) => { let supports_rule = lock.read_with(guard); let enabled = supports_rule.enabled; if enabled { let rules = &supports_rule.rules.read_with(guard).0; - f(rules, None) + f(NestedRulesResult::Rules(rules)) + } else { + f(NestedRulesResult::Rules(&[])) + } + } + CssRule::Document(ref lock) => { + if cfg!(feature = "gecko") { + let document_rule = lock.read_with(guard); + let rules = &document_rule.rules.read_with(guard).0; + f(NestedRulesResult::RulesWithDocument(rules, &document_rule)) } else { - f(&[], None) + unimplemented!() } } } @@ -460,6 +484,7 @@ impl ToCssWithGuard for CssRule { CssRule::Media(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::Supports(ref lock) => lock.read_with(guard).to_css(guard, dest), CssRule::Page(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Document(ref lock) => lock.read_with(guard).to_css(guard, dest), } } } @@ -654,6 +679,29 @@ impl ToCssWithGuard for StyleRule { #[cfg(feature = "servo")] pub type FontFaceRule = FontFaceRuleData; +#[derive(Debug)] +/// A @-moz-document rule +pub struct DocumentRule { + /// The parsed condition + pub condition: DocumentCondition, + /// Child rules + pub rules: Arc>, +} + +impl ToCssWithGuard for DocumentRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut W) -> fmt::Result + where W: fmt::Write { + try!(dest.write_str("@-moz-document ")); + try!(self.condition.to_css(dest)); + try!(dest.write_str(" {")); + for rule in self.rules.read_with(guard).0.iter() { + try!(dest.write_str(" ")); + try!(rule.to_css(guard, dest)); + } + dest.write_str(" }") + } +} + impl Stylesheet { /// Updates an empty stylesheet from a given string of text. pub fn update_from_str(existing: &Stylesheet, @@ -820,12 +868,24 @@ fn effective_rules(rules: &[CssRule], { for rule in rules { f(rule); - rule.with_nested_rules_and_mq(guard, |rules, mq| { - if let Some(media_queries) = mq { - if !media_queries.evaluate(device, quirks_mode) { - return - } - } + rule.with_nested_rules_mq_and_doc_rule(guard, |result| { + let rules = match result { + NestedRulesResult::Rules(rules) => { + rules + }, + NestedRulesResult::RulesWithMediaQueries(rules, media_queries) => { + if !media_queries.evaluate(device, quirks_mode) { + return; + } + rules + }, + NestedRulesResult::RulesWithDocument(rules, doc_rule) => { + if !doc_rule.condition.evaluate(device) { + return; + } + rules + }, + }; effective_rules(rules, device, quirks_mode, guard, f) }) } @@ -859,6 +919,7 @@ rule_filter! { effective_keyframes_rules(Keyframes => KeyframesRule), effective_supports_rules(Supports => SupportsRule), effective_page_rules(Page => PageRule), + effective_document_rules(Document => DocumentRule), } /// The stylesheet loader is the abstraction used to trigger network requests @@ -948,6 +1009,8 @@ enum AtRulePrelude { Keyframes(KeyframesName, Option), /// A @page rule prelude. Page, + /// A @document rule, with its conditional. + Document(DocumentCondition), } @@ -1171,6 +1234,14 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { Err(()) } }, + "-moz-document" => { + if cfg!(feature = "gecko") { + let cond = DocumentCondition::parse(self.context, input)?; + Ok(AtRuleType::WithBlock(AtRulePrelude::Document(cond))) + } else { + Err(()) + } + }, _ => Err(()) } } @@ -1221,6 +1292,16 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { Arc::new(self.shared_lock.wrap(declarations)) ))))) } + AtRulePrelude::Document(cond) => { + if cfg!(feature = "gecko") { + Ok(CssRule::Document(Arc::new(self.shared_lock.wrap(DocumentRule { + condition: cond, + rules: self.parse_nested_rules(input, CssRuleType::Document), + })))) + } else { + unreachable!() + } + } } } } diff --git a/components/style/stylist.rs b/components/style/stylist.rs index 54030358ddb7..e693a1d24e7d 100644 --- a/components/style/stylist.rs +++ b/components/style/stylist.rs @@ -42,6 +42,8 @@ use std::marker::PhantomData; use style_traits::viewport::ViewportConstraints; use stylearc::Arc; use stylesheets::{CssRule, FontFaceRule, Origin, StyleRule, Stylesheet, UserAgentStylesheets}; +#[cfg(feature = "servo")] +use stylesheets::NestedRulesResult; use thread_state; use viewport::{self, MaybeNew, ViewportRule}; @@ -596,12 +598,23 @@ impl Stylist { fn mq_eval_changed(guard: &SharedRwLockReadGuard, rules: &[CssRule], before: &Device, after: &Device, quirks_mode: QuirksMode) -> bool { for rule in rules { - let changed = rule.with_nested_rules_and_mq(guard, |rules, mq| { - if let Some(mq) = mq { - if mq.evaluate(before, quirks_mode) != mq.evaluate(after, quirks_mode) { - return true - } - } + let changed = rule.with_nested_rules_mq_and_doc_rule(guard, + |result| { + let rules = match result { + NestedRulesResult::Rules(rules) => rules, + NestedRulesResult::RulesWithMediaQueries(rules, mq) => { + if mq.evaluate(before, quirks_mode) != mq.evaluate(after, quirks_mode) { + return true; + } + rules + }, + NestedRulesResult::RulesWithDocument(rules, doc_rule) => { + if !doc_rule.condition.evaluate(before) { + return false; + } + rules + }, + }; mq_eval_changed(guard, rules, before, after, quirks_mode) }); if changed { diff --git a/ports/geckolib/Cargo.toml b/ports/geckolib/Cargo.toml index 6cb93fa3466a..382a1dbafa36 100644 --- a/ports/geckolib/Cargo.toml +++ b/ports/geckolib/Cargo.toml @@ -20,6 +20,7 @@ cssparser = "0.13" env_logger = {version = "0.4", default-features = false} # disable `regex` to reduce code size libc = "0.2" log = {version = "0.3.5", features = ["release_max_level_info"]} +nsstring_vendor = {path = "../../components/style/gecko_bindings/nsstring_vendor"} parking_lot = "0.3" selectors = {path = "../../components/selectors"} style = {path = "../../components/style", features = ["gecko"]} diff --git a/tests/unit/style/media_queries.rs b/tests/unit/style/media_queries.rs index d538225d9d88..f7ce523aa168 100644 --- a/tests/unit/style/media_queries.rs +++ b/tests/unit/style/media_queries.rs @@ -13,7 +13,7 @@ use style::media_queries::*; use style::servo::media_queries::*; use style::shared_lock::{SharedRwLock, SharedRwLockReadGuard}; use style::stylearc::Arc; -use style::stylesheets::{Stylesheet, Origin, CssRule}; +use style::stylesheets::{Stylesheet, Origin, CssRule, NestedRulesResult}; use style::values::specified; use style_traits::ToCss; @@ -52,11 +52,16 @@ fn media_queries(guard: &SharedRwLockReadGuard, rules: &[CssRule], f: &mut F) where F: FnMut(&MediaList), { for rule in rules { - rule.with_nested_rules_and_mq(guard, |rules, mq| { - if let Some(mq) = mq { - f(mq) + rule.with_nested_rules_mq_and_doc_rule(guard, |result| { + match result { + NestedRulesResult::Rules(rules) | + NestedRulesResult::RulesWithDocument(rules, _) => { + media_queries(guard, rules, f) + }, + NestedRulesResult::RulesWithMediaQueries(_, mq) => { + f(mq) + } } - media_queries(guard, rules, f) }) } }