From 3b14c0705133cc014f44c612072fe8b80878b3a8 Mon Sep 17 00:00:00 2001 From: James Gilbertson Date: Mon, 23 Mar 2015 00:03:10 -0600 Subject: [PATCH] Implement parsing of an @viewport rule --- components/style/lib.rs | 2 +- components/style/parser.rs | 2 + components/style/stylesheets.rs | 19 ++ components/style/values.rs | 45 +++-- components/style/viewport.rs | 302 ++++++++++++++++++++++++++++++++ tests/unit/style/lib.rs | 1 + tests/unit/style/viewport.rs | 202 +++++++++++++++++++++ 7 files changed, 557 insertions(+), 16 deletions(-) create mode 100644 components/style/viewport.rs create mode 100644 tests/unit/style/viewport.rs diff --git a/components/style/lib.rs b/components/style/lib.rs index 109216ecf553..f0c3e17c5505 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -50,6 +50,7 @@ pub mod media_queries; pub mod font_face; pub mod legacy; pub mod animation; +pub mod viewport; macro_rules! reexport_computed_values { ( $( $name: ident )+ ) => { @@ -63,4 +64,3 @@ macro_rules! reexport_computed_values { } } longhand_properties_idents!(reexport_computed_values); - diff --git a/components/style/parser.rs b/components/style/parser.rs index c9d1891d708b..0e3ea92db528 100644 --- a/components/style/parser.rs +++ b/components/style/parser.rs @@ -11,6 +11,7 @@ use log; use stylesheets::Origin; pub struct ParserContext<'a> { + pub stylesheet_origin: Origin, pub base_url: &'a Url, pub selector_context: SelectorParserContext, } @@ -20,6 +21,7 @@ impl<'a> ParserContext<'a> { let mut selector_context = SelectorParserContext::new(); selector_context.in_user_agent_stylesheet = stylesheet_origin == Origin::UserAgent; ParserContext { + stylesheet_origin: stylesheet_origin, base_url: base_url, selector_context: selector_context, } diff --git a/components/style/stylesheets.rs b/components/style/stylesheets.rs index 90f4a7510df1..65368452665b 100644 --- a/components/style/stylesheets.rs +++ b/components/style/stylesheets.rs @@ -19,6 +19,7 @@ use properties::{PropertyDeclarationBlock, parse_property_declaration_list}; use media_queries::{Device, MediaQueryList, parse_media_query_list}; use font_face::{FontFaceRule, parse_font_face_block}; use util::smallvec::SmallVec2; +use viewport::ViewportRule; /// Each style rule has an origin, which determines where it enters the cascade. @@ -53,6 +54,7 @@ pub enum CSSRule { Style(StyleRule), Media(MediaRule), FontFace(FontFaceRule), + Viewport(ViewportRule), } #[derive(Debug, PartialEq)] @@ -216,6 +218,7 @@ pub mod rule_filter { use std::marker::PhantomData; use super::{CSSRule, MediaRule, StyleRule}; use super::super::font_face::FontFaceRule; + use super::super::viewport::ViewportRule; macro_rules! rule_filter { ($variant:ident -> $value:ty) => { @@ -259,6 +262,7 @@ pub mod rule_filter { rule_filter!(FontFace -> FontFaceRule); rule_filter!(Media -> MediaRule); rule_filter!(Style -> StyleRule); + rule_filter!(Viewport -> ViewportRule); } /// Extension methods for `CSSRule` iterators. @@ -271,6 +275,9 @@ pub trait CSSRuleIteratorExt<'a>: Iterator { /// Yield only style rules. fn style(self) -> rule_filter::Style<'a, Self>; + + /// Yield only @viewport rules. + fn viewport(self) -> rule_filter::Viewport<'a, Self>; } impl<'a, I> CSSRuleIteratorExt<'a> for I where I: Iterator { @@ -288,6 +295,11 @@ impl<'a, I> CSSRuleIteratorExt<'a> for I where I: Iterator { fn style(self) -> rule_filter::Style<'a, I> { rule_filter::Style::new(self) } + + #[inline] + fn viewport(self) -> rule_filter::Viewport<'a, I> { + rule_filter::Viewport::new(self) + } } fn parse_nested_rules(context: &ParserContext, input: &mut Parser) -> Vec { @@ -324,6 +336,7 @@ enum State { enum AtRulePrelude { FontFace, Media(MediaQueryList), + Viewport, } @@ -414,6 +427,9 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { }, "font-face" => { Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace)) + }, + "viewport" => { + Ok(AtRuleType::WithBlock(AtRulePrelude::Viewport)) } _ => Err(()) } @@ -430,6 +446,9 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { rules: parse_nested_rules(self.context, input), })) } + AtRulePrelude::Viewport => { + ViewportRule::parse(input, self.context).map(CSSRule::Viewport) + } } } } diff --git a/components/style/values.rs b/components/style/values.rs index 88ae4b0417ca..81db7753cac7 100644 --- a/components/style/values.rs +++ b/components/style/values.rs @@ -86,6 +86,22 @@ pub mod specified { use util::geometry::Au; use super::CSSFloat; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum AllowedNumericType { + All, + NonNegative + } + + impl AllowedNumericType { + #[inline] + pub fn is_ok(&self, value: f32) -> bool { + match self { + &AllowedNumericType::All => true, + &AllowedNumericType::NonNegative => value >= 0., + } + } + } + #[derive(Clone, PartialEq, Debug)] pub struct CSSColor { pub parsed: cssparser::Color, @@ -397,33 +413,32 @@ pub mod specified { } } } + impl LengthOrPercentageOrAuto { - fn parse_internal(input: &mut Parser, negative_ok: bool) - -> Result { + fn parse_internal(input: &mut Parser, context: &AllowedNumericType) + -> Result + { match try!(input.next()) { - Token::Dimension(ref value, ref unit) if negative_ok || value.value >= 0. => { + Token::Dimension(ref value, ref unit) if context.is_ok(value.value) => { Length::parse_dimension(value.value, unit) - .map(LengthOrPercentageOrAuto::Length) - } - Token::Percentage(ref value) if negative_ok || value.unit_value >= 0. => { - Ok(LengthOrPercentageOrAuto::Percentage(value.unit_value)) - } - Token::Number(ref value) if value.value == 0. => { - Ok(LengthOrPercentageOrAuto::Length(Length::Absolute(Au(0)))) - } - Token::Ident(ref value) if value.eq_ignore_ascii_case("auto") => { - Ok(LengthOrPercentageOrAuto::Auto) + .map(LengthOrPercentageOrAuto::Length) } + Token::Percentage(ref value) if context.is_ok(value.unit_value) => + Ok(LengthOrPercentageOrAuto::Percentage(value.unit_value)), + Token::Number(ref value) if context.is_ok(value.value) => + Ok(LengthOrPercentageOrAuto::Length(Length::Absolute(Au(0)))), + Token::Ident(ref value) if value.eq_ignore_ascii_case("auto") => + Ok(LengthOrPercentageOrAuto::Auto), _ => Err(()) } } #[inline] pub fn parse(input: &mut Parser) -> Result { - LengthOrPercentageOrAuto::parse_internal(input, /* negative_ok = */ true) + LengthOrPercentageOrAuto::parse_internal(input, &AllowedNumericType::All) } #[inline] pub fn parse_non_negative(input: &mut Parser) -> Result { - LengthOrPercentageOrAuto::parse_internal(input, /* negative_ok = */ false) + LengthOrPercentageOrAuto::parse_internal(input, &AllowedNumericType::NonNegative) } } diff --git a/components/style/viewport.rs b/components/style/viewport.rs new file mode 100644 index 000000000000..e9588b27bc63 --- /dev/null +++ b/components/style/viewport.rs @@ -0,0 +1,302 @@ +/* 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::{Parser, DeclarationListParser, AtRuleParser, DeclarationParser, ToCss, parse_important}; +use parser::{ParserContext, log_css_error}; +use stylesheets::Origin; +use values::specified::{AllowedNumericType, Length, LengthOrPercentageOrAuto}; + +use std::ascii::AsciiExt; +use std::collections::hash_map::{Entry, HashMap}; +use std::fmt; +use std::intrinsics; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ViewportDescriptor { + MinWidth(LengthOrPercentageOrAuto), + MaxWidth(LengthOrPercentageOrAuto), + + MinHeight(LengthOrPercentageOrAuto), + MaxHeight(LengthOrPercentageOrAuto), + + Zoom(Zoom), + MinZoom(Zoom), + MaxZoom(Zoom), + + UserZoom(UserZoom), + Orientation(Orientation) +} + +/// Zoom is a number | percentage | auto +/// See http://dev.w3.org/csswg/css-device-adapt/#descdef-viewport-zoom +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Zoom { + Number(f32), + Percentage(f32), + Auto, +} + +impl ToCss for Zoom { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write + { + match self { + &Zoom::Number(number) => write!(dest, "{}", number), + &Zoom::Percentage(percentage) => write!(dest, "{}%", percentage * 100.), + &Zoom::Auto => write!(dest, "auto") + } + } +} + +impl Zoom { + pub fn parse(input: &mut Parser) -> Result { + use cssparser::Token; + + match try!(input.next()) { + Token::Percentage(ref value) if AllowedNumericType::NonNegative.is_ok(value.unit_value) => + Ok(Zoom::Percentage(value.unit_value)), + Token::Number(ref value) if AllowedNumericType::NonNegative.is_ok(value.value) => + Ok(Zoom::Number(value.value)), + Token::Ident(ref value) if value.eq_ignore_ascii_case("auto") => + Ok(Zoom::Auto), + _ => Err(()) + } + } + + #[inline] + pub fn to_f32(&self) -> Option { + match self { + &Zoom::Number(number) => Some(number as f32), + &Zoom::Percentage(percentage) => Some(percentage as f32), + &Zoom::Auto => None + } + } +} + +define_css_keyword_enum!(UserZoom: + "zoom" => Zoom, + "fixed" => Fixed); + +define_css_keyword_enum!(Orientation: + "auto" => Auto, + "portrait" => Portrait, + "landscape" => Landscape); + +struct ViewportRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b> +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ViewportDescriptorDeclaration { + pub origin: Origin, + pub descriptor: ViewportDescriptor, + pub important: bool +} + +impl ViewportDescriptorDeclaration { + pub fn new(origin: Origin, + descriptor: ViewportDescriptor, + important: bool) -> ViewportDescriptorDeclaration + { + ViewportDescriptorDeclaration { + origin: origin, + descriptor: descriptor, + important: important + } + } +} + +fn parse_shorthand(input: &mut Parser) -> Result<[LengthOrPercentageOrAuto; 2], ()> { + let min = try!(LengthOrPercentageOrAuto::parse_non_negative(input)); + match input.try(|input| LengthOrPercentageOrAuto::parse_non_negative(input)) { + Err(()) => Ok([min.clone(), min]), + Ok(max) => Ok([min, max]) + } +} + +impl<'a, 'b> AtRuleParser for ViewportRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = Vec; +} + +impl<'a, 'b> DeclarationParser for ViewportRuleParser<'a, 'b> { + type Declaration = Vec; + + fn parse_value(&self, name: &str, input: &mut Parser) -> Result, ()> { + macro_rules! declaration { + ($declaration:ident($parse:path)) => { + declaration!($declaration(value: try!($parse(input)), + important: input.try(parse_important).is_ok())) + }; + ($declaration:ident(value: $value:expr, important: $important:expr)) => { + ViewportDescriptorDeclaration::new( + self.context.stylesheet_origin, + ViewportDescriptor::$declaration($value), + $important) + } + } + + macro_rules! ok { + ($declaration:ident($parse:path)) => { + Ok(vec![declaration!($declaration($parse))]) + }; + (shorthand -> [$min:ident, $max:ident]) => {{ + let shorthand = try!(parse_shorthand(input)); + let important = input.try(parse_important).is_ok(); + + Ok(vec![declaration!($min(value: shorthand[0], important: important)), + declaration!($max(value: shorthand[1], important: important))]) + }} + } + + match name { + n if n.eq_ignore_ascii_case("min-width") => + ok!(MinWidth(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("max-width") => + ok!(MaxWidth(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("width") => + ok!(shorthand -> [MinWidth, MaxWidth]), + + n if n.eq_ignore_ascii_case("min-height") => + ok!(MinHeight(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("max-height") => + ok!(MaxHeight(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("height") => + ok!(shorthand -> [MinHeight, MaxHeight]), + + n if n.eq_ignore_ascii_case("zoom") => + ok!(Zoom(Zoom::parse)), + n if n.eq_ignore_ascii_case("min-zoom") => + ok!(MinZoom(Zoom::parse)), + n if n.eq_ignore_ascii_case("max-zoom") => + ok!(MaxZoom(Zoom::parse)), + + n if n.eq_ignore_ascii_case("user-zoom") => + ok!(UserZoom(UserZoom::parse)), + n if n.eq_ignore_ascii_case("orientation") => + ok!(Orientation(Orientation::parse)), + + _ => Err(()), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct ViewportRule { + pub declarations: Vec +} + +impl ViewportRule { + pub fn parse<'a>(input: &mut Parser, context: &'a ParserContext) + -> Result + { + let parser = ViewportRuleParser { context: context }; + + let mut errors = vec![]; + let valid_declarations = DeclarationListParser::new(input, parser) + .filter_map(|result| { + match result { + Ok(declarations) => Some(declarations), + Err(range) => { + errors.push(range); + None + } + } + }) + .flat_map(|declarations| declarations.into_iter()) + .collect::>(); + + for range in errors { + let pos = range.start; + let message = format!("Unsupported @viewport descriptor declaration: '{}'", + input.slice(range)); + log_css_error(input, pos, &*message); + } + + Ok(ViewportRule { declarations: valid_declarations.iter().cascade() }) + } +} + +pub trait ViewportRuleCascade: Iterator + Sized { + fn cascade(self) -> ViewportRule; +} + +impl<'a, I> ViewportRuleCascade for I + where I: Iterator +{ + #[inline] + fn cascade(self) -> ViewportRule { + ViewportRule { + declarations: self.flat_map(|r| r.declarations.iter()).cascade() + } + } +} + +trait ViewportDescriptorDeclarationCascade: Iterator + Sized { + fn cascade(self) -> Vec; +} + +/// Computes the cascade precedence as according to +/// http://dev.w3.org/csswg/css-cascade/#cascade-origin +fn cascade_precendence(origin: Origin, important: bool) -> u8 { + match (origin, important) { + (Origin::UserAgent, true) => 1, + (Origin::User, true) => 2, + (Origin::Author, true) => 3, + (Origin::Author, false) => 4, + (Origin::User, false) => 5, + (Origin::UserAgent, false) => 6, + } +} + +impl ViewportDescriptorDeclaration { + fn higher_or_equal_precendence(&self, other: &ViewportDescriptorDeclaration) -> bool { + let self_precedence = cascade_precendence(self.origin, self.important); + let other_precedence = cascade_precendence(other.origin, other.important); + + self_precedence <= other_precedence + } +} + +fn cascade<'a, I>(iter: I) -> Vec + where I: Iterator +{ + let mut declarations: HashMap = HashMap::new(); + + // index is used to reconstruct order of appearance after all declarations + // have been added to the map + let mut index = 0; + for declaration in iter { + let descriptor = unsafe { + intrinsics::discriminant_value(&declaration.descriptor) + }; + + match declarations.entry(descriptor) { + Entry::Occupied(mut entry) => { + if declaration.higher_or_equal_precendence(entry.get().1) { + entry.insert((index, declaration)); + index += 1; + } + } + Entry::Vacant(entry) => { + entry.insert((index, declaration)); + index += 1; + } + } + } + + // convert to a list and sort the descriptors by order of appearance + let mut declarations: Vec<_> = declarations.into_iter().map(|kv| kv.1).collect(); + declarations.sort_by(|a, b| a.0.cmp(&b.0)); + declarations.into_iter().map(|id| *id.1).collect::>() +} + +impl<'a, I> ViewportDescriptorDeclarationCascade for I + where I: Iterator +{ + #[inline] + fn cascade(self) -> Vec { + cascade(self) + } +} diff --git a/tests/unit/style/lib.rs b/tests/unit/style/lib.rs index 9e379d1336bc..a90707b522c6 100644 --- a/tests/unit/style/lib.rs +++ b/tests/unit/style/lib.rs @@ -16,6 +16,7 @@ extern crate util; #[cfg(test)] mod stylesheets; #[cfg(test)] mod media_queries; +#[cfg(test)] mod viewport; #[cfg(test)] mod writing_modes { use util::logical_geometry::WritingMode; diff --git a/tests/unit/style/viewport.rs b/tests/unit/style/viewport.rs new file mode 100644 index 000000000000..ebd2360b6aab --- /dev/null +++ b/tests/unit/style/viewport.rs @@ -0,0 +1,202 @@ +/* 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::Parser; +use geom::size::TypedSize2D; +use geom::scale_factor::ScaleFactor; +use style::media_queries::{Device, MediaType}; +use style::parser::ParserContext; +use style::stylesheets::{Origin, Stylesheet, CSSRuleIteratorExt}; +use style::values::specified::{Length, LengthOrPercentageOrAuto}; +use style::viewport::*; +use url::Url; + +macro_rules! stylesheet { + ($css:expr, $origin:ident) => { + Stylesheet::from_str($css, + Url::parse("http://localhost").unwrap(), + Origin::$origin); + } +} + +fn test_viewport_rule(css: &str, + device: &Device, + callback: F) + where F: Fn(&Vec, &str) +{ + let stylesheet = stylesheet!(css, Author); + let mut rule_count = 0; + for rule in stylesheet.effective_rules(&device).viewport() { + rule_count += 1; + callback(&rule.declarations, css); + } + assert!(rule_count > 0); +} + +macro_rules! assert_declarations_len { + ($declarations:ident == 1) => { + assert!($declarations.len() == 1, + "expected 1 declaration; have {}: {:?})", + $declarations.len(), $declarations) + }; + ($declarations:ident == $len:expr) => { + assert!($declarations.len() == $len, + "expected {} declarations; have {}: {:?})", + $len, $declarations.len(), $declarations) + } +} + +#[test] +fn empty_viewport_rule() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + test_viewport_rule("@viewport {}", &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 0); + }); +} + +macro_rules! assert_declaration_eq { + ($d:expr, $origin:ident, $expected:ident: $value:expr) => {{ + assert_eq!($d.origin, Origin::$origin); + assert_eq!($d.descriptor, ViewportDescriptor::$expected($value)); + assert!($d.important == false, "descriptor should not be !important"); + }}; + ($d:expr, $origin:ident, $expected:ident: $value:expr, !important) => {{ + assert_eq!($d.origin, Origin::$origin); + assert_eq!($d.descriptor, ViewportDescriptor::$expected($value)); + assert!($d.important == true, "descriptor should be !important"); + }}; +} + +#[test] +fn simple_viewport_rules() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + test_viewport_rule("@viewport { width: auto; height: auto;\ + zoom: auto; min-zoom: 0; max-zoom: 200%;\ + user-zoom: zoom; orientation: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 9); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[2], Author, MinHeight: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[3], Author, MaxHeight: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[4], Author, Zoom: Zoom::Auto); + assert_declaration_eq!(&declarations[5], Author, MinZoom: Zoom::Number(0.)); + assert_declaration_eq!(&declarations[6], Author, MaxZoom: Zoom::Percentage(2.)); + assert_declaration_eq!(&declarations[7], Author, UserZoom: UserZoom::Zoom); + assert_declaration_eq!(&declarations[8], Author, Orientation: Orientation::Auto); + }); + + test_viewport_rule("@viewport { min-width: 200px; max-width: auto;\ + min-height: 200px; max-height: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 4); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[2], Author, MinHeight: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + assert_declaration_eq!(&declarations[3], Author, MaxHeight: LengthOrPercentageOrAuto::Auto); + }); +} + +#[test] +fn cascading_within_viewport_rule() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + // normal order of appearance + test_viewport_rule("@viewport { min-width: 200px; min-width: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 1); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + }); + + // !important order of appearance + test_viewport_rule("@viewport { min-width: 200px !important; min-width: auto !important; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 1); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + }); + + // !important vs normal + test_viewport_rule("@viewport { min-width: auto !important; min-width: 200px; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 1); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + }); + + // normal longhands vs normal shorthand + test_viewport_rule("@viewport { min-width: 200px; max-width: 200px; width: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + }); + + // normal shorthand vs normal longhands + test_viewport_rule("@viewport { width: 200px; min-width: auto; max-width: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + }); + + // one !important longhand vs normal shorthand + test_viewport_rule("@viewport { min-width: auto !important; width: 200px; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + }); + + // both !important longhands vs normal shorthand + test_viewport_rule("@viewport { min-width: auto !important; max-width: auto !important; width: 200px; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto, !important); + }); +} + +#[test] +fn multiple_stylesheets_cascading() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + let stylesheets = vec![ + stylesheet!("@viewport { min-width: 100px; min-height: 100px; zoom: 1; }", UserAgent), + stylesheet!("@viewport { min-width: 200px; min-height: 200px; }", User), + stylesheet!("@viewport { min-width: 300px; }", Author)]; + + let declarations = stylesheets.iter() + .flat_map(|s| s.effective_rules(&device).viewport()) + .cascade() + .declarations; + assert_declarations_len!(declarations == 3); + assert_declaration_eq!(&declarations[0], UserAgent, Zoom: Zoom::Number(1.)); + assert_declaration_eq!(&declarations[1], User, MinHeight: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + assert_declaration_eq!(&declarations[2], Author, MinWidth: LengthOrPercentageOrAuto::Length(Length::from_px(300.))); + + let stylesheets = vec![ + stylesheet!("@viewport { min-width: 100px !important; }", UserAgent), + stylesheet!("@viewport { min-width: 200px !important; min-height: 200px !important; }", User), + stylesheet!("@viewport { min-width: 300px !important; min-height: 300px !important; zoom: 3 !important; }", Author)]; + + let declarations = stylesheets.iter() + .flat_map(|s| s.effective_rules(&device).viewport()) + .cascade() + .declarations; + assert_declarations_len!(declarations == 3); + assert_declaration_eq!(&declarations[0], UserAgent, MinWidth: LengthOrPercentageOrAuto::Length(Length::from_px(100.)), !important); + assert_declaration_eq!(&declarations[1], User, MinHeight: LengthOrPercentageOrAuto::Length(Length::from_px(200.)), !important); + assert_declaration_eq!(&declarations[2], Author, Zoom: Zoom::Number(3.), !important); +}