From ab3dd48482f8b7750a03d2aec17a2c6a02be26a5 Mon Sep 17 00:00:00 2001 From: Glenn Watson Date: Tue, 14 Oct 2014 18:36:29 -0600 Subject: [PATCH] servo: Merge #3610 - Implement media queries parser and matching. Improves mobile first sites like bootstrap3 (from glennw:media-queries); r=SimonSapin Source-Repo: https://github.com/servo/servo Source-Revision: 3eb6b17137964fc12779eca9597fa77930440138 --- servo/components/layout/layout_task.rs | 13 +- servo/components/style/font_face.rs | 8 +- servo/components/style/lib.rs | 1 + servo/components/style/media_queries.rs | 682 ++++++++++++++++++-- servo/components/style/parsing_utils.rs | 19 + servo/components/style/selector_matching.rs | 10 +- servo/components/style/stylesheets.rs | 17 +- 7 files changed, 686 insertions(+), 64 deletions(-) diff --git a/servo/components/layout/layout_task.rs b/servo/components/layout/layout_task.rs index 2ff6d921490d..68ab0b863da1 100644 --- a/servo/components/layout/layout_task.rs +++ b/servo/components/layout/layout_task.rs @@ -64,6 +64,7 @@ use std::comm::{channel, Sender, Receiver, Select}; use std::mem; use std::ptr; use style::{AuthorOrigin, Stylesheet, Stylist, TNode, iter_font_face_rules}; +use style::{Device, Screen}; use sync::{Arc, Mutex, MutexGuard}; use url::Url; @@ -143,6 +144,10 @@ pub struct LayoutTask { /// /// All the other elements of this struct are read-only. pub rw_data: Arc>, + + /// The media queries device state. + /// TODO: Handle updating this when window size changes etc. + pub device: Device, } struct LayoutImageResponder { @@ -252,6 +257,7 @@ impl LayoutTask { -> LayoutTask { let local_image_cache = Arc::new(Mutex::new(LocalImageCache::new(image_cache_task.clone()))); let screen_size = Size2D(Au(0), Au(0)); + let device = Device::new(Screen, opts.initial_window_size.as_f32()); let parallel_traversal = if opts.layout_threads != 1 { Some(WorkQueue::new("LayoutWorker", opts.layout_threads, ptr::null())) } else { @@ -272,12 +278,13 @@ impl LayoutTask { font_cache_task: font_cache_task, opts: opts.clone(), first_reflow: Cell::new(true), + device: device, rw_data: Arc::new(Mutex::new( LayoutTaskData { local_image_cache: local_image_cache, screen_size: screen_size, display_list: None, - stylist: box Stylist::new(), + stylist: box Stylist::new(&device), parallel_traversal: parallel_traversal, dirty: Rect::zero(), generation: 0, @@ -469,11 +476,11 @@ impl LayoutTask { fn handle_add_stylesheet<'a>(&'a self, sheet: Stylesheet, possibly_locked_rw_data: &mut Option>) { // Find all font-face rules and notify the font cache of them. // GWTODO: Need to handle unloading web fonts (when we handle unloading stylesheets!) - iter_font_face_rules(&sheet, |family, src| { + iter_font_face_rules(&sheet, &self.device, |family, src| { self.font_cache_task.add_web_font(family.to_string(), (*src).clone()); }); let mut rw_data = self.lock_rw_data(possibly_locked_rw_data); - rw_data.stylist.add_stylesheet(sheet, AuthorOrigin); + rw_data.stylist.add_stylesheet(sheet, AuthorOrigin, &self.device); rw_data.stylesheet_dirty = true; LayoutTask::return_rw_data(possibly_locked_rw_data, rw_data); } diff --git a/servo/components/style/font_face.rs b/servo/components/style/font_face.rs index 62a65db96a53..b2d20f52675f 100644 --- a/servo/components/style/font_face.rs +++ b/servo/components/style/font_face.rs @@ -10,17 +10,17 @@ use parsing_utils::{BufferedIter, ParserIter, parse_slice_comma_separated}; use properties::longhands::font_family::parse_one_family; use properties::computed_values::font_family::FamilyName; use stylesheets::{CSSRule, CSSFontFaceRule, CSSStyleRule, CSSMediaRule}; -use media_queries::{Device, Screen}; +use media_queries::Device; use url::{Url, UrlParser}; -pub fn iter_font_face_rules_inner(rules: &[CSSRule], callback: |family: &str, source: &Source|) { - let device = &Device { media_type: Screen }; // TODO, use Print when printing +pub fn iter_font_face_rules_inner(rules: &[CSSRule], device: &Device, + callback: |family: &str, source: &Source|) { for rule in rules.iter() { match *rule { CSSStyleRule(_) => {}, CSSMediaRule(ref rule) => if rule.media_queries.evaluate(device) { - iter_font_face_rules_inner(rule.rules.as_slice(), |f, s| callback(f, s)) + iter_font_face_rules_inner(rule.rules.as_slice(), device, |f, s| callback(f, s)) }, CSSFontFaceRule(ref rule) => { for source in rule.sources.iter() { diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs index fc3fd43a94df..8b516151025d 100644 --- a/servo/components/style/lib.rs +++ b/servo/components/style/lib.rs @@ -35,6 +35,7 @@ extern crate "util" as servo_util; // Public API +pub use media_queries::{Device, Screen}; pub use stylesheets::{Stylesheet, iter_font_face_rules}; pub use selector_matching::{Stylist, StylesheetOrigin, UserAgentOrigin, AuthorOrigin, UserOrigin}; pub use selector_matching::{DeclarationBlock, matches,matches_simple_selector}; diff --git a/servo/components/style/media_queries.rs b/servo/components/style/media_queries.rs index af12b4fe638c..d5e9dad9f121 100644 --- a/servo/components/style/media_queries.rs +++ b/servo/components/style/media_queries.rs @@ -7,30 +7,68 @@ use cssparser::parse_rule_list; use cssparser::ast::*; use errors::{ErrorLoggerIterator, log_css_error}; +use geom::size::TypedSize2D; use stylesheets::{CSSRule, CSSMediaRule, parse_style_rule, parse_nested_at_rule}; use namespaces::NamespaceMap; +use parsing_utils::{BufferedIter, ParserIter}; +use properties::common_types::*; +use properties::longhands; +use servo_util::geometry::ScreenPx; use url::Url; - pub struct MediaRule { pub media_queries: MediaQueryList, pub rules: Vec, } - pub struct MediaQueryList { - // "not all" is omitted from the list. - // An empty list never matches. media_queries: Vec } -// For now, this is a "Level 2 MQ", ie. a media type. -pub struct MediaQuery { - media_type: MediaQueryType, - // TODO: Level 3 MQ expressions +pub enum Range { + Min(T), + Max(T), + Eq(T), } +impl Range { + fn evaluate(&self, value: T) -> bool { + match *self { + Min(ref width) => { value >= *width }, + Max(ref width) => { value <= *width }, + Eq(ref width) => { value == *width }, + } + } +} +pub enum Expression { + Width(Range), +} + +#[deriving(PartialEq)] +pub enum Qualifier { + Only, + Not, +} + +pub struct MediaQuery { + qualifier: Option, + media_type: MediaQueryType, + expressions: Vec, +} + +impl MediaQuery { + pub fn new(qualifier: Option, media_type: MediaQueryType, + expressions: Vec) -> MediaQuery { + MediaQuery { + qualifier: qualifier, + media_type: media_type, + expressions: expressions, + } + } +} + +#[deriving(PartialEq)] pub enum MediaQueryType { All, // Always true MediaType_(MediaType), @@ -40,13 +78,22 @@ pub enum MediaQueryType { pub enum MediaType { Screen, Print, + Unknown, } pub struct Device { pub media_type: MediaType, - // TODO: Level 3 MQ data: viewport size, etc. + pub viewport_size: TypedSize2D, } +impl Device { + pub fn new(media_type: MediaType, viewport_size: TypedSize2D) -> Device { + Device { + media_type: media_type, + viewport_size: viewport_size, + } + } +} pub fn parse_media_rule(rule: AtRule, parent_rules: &mut Vec, namespaces: &NamespaceMap, base_url: &Url) { @@ -72,60 +119,597 @@ pub fn parse_media_rule(rule: AtRule, parent_rules: &mut Vec, })) } +fn parse_value_as_length(value: &ComponentValue) -> Result { + let length = try!(specified::Length::parse_non_negative(value)); -pub fn parse_media_query_list(input: &[ComponentValue]) -> MediaQueryList { - let iter = &mut input.skip_whitespace(); - let mut next = iter.next(); - if next.is_none() { - return MediaQueryList{ media_queries: vec!(MediaQuery{media_type: All}) } - } - let mut queries = vec!(); - loop { - let mq = match next { - Some(&Ident(ref value)) => { - match value.as_slice().to_ascii_lower().as_slice() { - "screen" => Some(MediaQuery{ media_type: MediaType_(Screen) }), - "print" => Some(MediaQuery{ media_type: MediaType_(Print) }), - "all" => Some(MediaQuery{ media_type: All }), - _ => None + // http://dev.w3.org/csswg/mediaqueries3/ - Section 6 + // em units are relative to the initial font-size. + let initial_font_size = longhands::font_size::get_initial_value(); + Ok(computed::compute_Au_with_font_size(length, initial_font_size)) +} + +fn parse_media_query_expression(iter: ParserIter) -> Result { + // Expect a parenthesis block with the condition + match iter.next() { + Some(&ParenthesisBlock(ref block)) => { + let iter = &mut BufferedIter::new(block.as_slice().skip_whitespace()); + + // Parse the variable (e.g. min-width) + let variable = match iter.next() { + Some(&Ident(ref value)) => value, + _ => return Err(()) + }; + + // Ensure a colon follows + match iter.next() { + Some(&Colon) => {}, + _ => return Err(()) + } + + // Retrieve the value + let value = try!(iter.next_as_result()); + + // TODO: Handle other media query types + let expression = match variable.as_slice().to_ascii_lower().as_slice() { + "min-width" => { + let au = try!(parse_value_as_length(value)); + Width(Min(au)) } - }, - _ => None - }; - match iter.next() { - None => { - for mq in mq.into_iter() { - queries.push(mq); + "max-width" => { + let au = try!(parse_value_as_length(value)); + Width(Max(au)) } - return MediaQueryList{ media_queries: queries } - }, - Some(&Comma) => { - for mq in mq.into_iter() { - queries.push(mq); - } - }, - // Ingnore this comma-separated part - _ => loop { - match iter.next() { - Some(&Comma) => break, - None => return MediaQueryList{ media_queries: queries }, - _ => (), - } - }, + _ => return Err(()) + }; + + if iter.is_eof() { + Ok(expression) + } else { + Err(()) + } } - next = iter.next(); + _ => Err(()) } } +fn parse_media_query(iter: ParserIter) -> Result { + let mut expressions = vec!(); + + // Check for optional 'only' or 'not' + let qualifier = match iter.next() { + Some(&Ident(ref value)) if value.as_slice().to_ascii_lower().as_slice() == "only" => Some(Only), + Some(&Ident(ref value)) if value.as_slice().to_ascii_lower().as_slice() == "not" => Some(Not), + Some(component_value) => { + iter.push_back(component_value); + None + } + None => return Err(()), // Empty queries are invalid + }; + + // Check for media type + let media_type = match iter.next() { + Some(&Ident(ref value)) => { + match value.as_slice().to_ascii_lower().as_slice() { + "screen" => MediaType_(Screen), + "print" => MediaType_(Print), + "all" => All, + _ => MediaType_(Unknown), // Unknown media types never match + } + } + Some(component_value) => { + // Media type is only optional if qualifier is not specified. + if qualifier.is_some() { + return Err(()); + } + iter.push_back(component_value); + + // If no qualifier and media type present, an expression should exist here + let expression = try!(parse_media_query_expression(iter)); + expressions.push(expression); + + All + } + None => return Err(()), + }; + + // Parse any subsequent expressions + loop { + // Each expression should begin with and + match iter.next() { + Some(&Ident(ref value)) => { + match value.as_slice().to_ascii_lower().as_slice() { + "and" => { + let expression = try!(parse_media_query_expression(iter)); + expressions.push(expression); + } + _ => return Err(()), + } + } + Some(component_value) => { + iter.push_back(component_value); + break; + } + None => break, + } + } + + Ok(MediaQuery::new(qualifier, media_type, expressions)) +} + +pub fn parse_media_query_list(input: &[ComponentValue]) -> MediaQueryList { + let iter = &mut BufferedIter::new(input.skip_whitespace()); + let mut media_queries = vec!(); + + if iter.is_eof() { + media_queries.push(MediaQuery::new(None, All, vec!())); + } else { + loop { + // Attempt to parse a media query. + let media_query_result = parse_media_query(iter); + + // Skip until next query or end + let mut trailing_tokens = false; + let mut more_queries = false; + loop { + match iter.next() { + Some(&Comma) => { + more_queries = true; + break; + } + Some(_) => trailing_tokens = true, + None => break, + } + } + + // Add the media query if it was valid and no trailing tokens were found. + // Otherwse, create a 'not all' media query, that will never match. + let media_query = match (media_query_result, trailing_tokens) { + (Ok(media_query), false) => media_query, + _ => MediaQuery::new(Some(Not), All, vec!()), + }; + media_queries.push(media_query); + + if !more_queries { + break; + } + } + } + + MediaQueryList { media_queries: media_queries } +} impl MediaQueryList { pub fn evaluate(&self, device: &Device) -> bool { + // Check if any queries match (OR condition) self.media_queries.iter().any(|mq| { - match mq.media_type { + // Check if media matches. Unknown media never matches. + let media_match = match mq.media_type { + MediaType_(Unknown) => false, MediaType_(media_type) => media_type == device.media_type, All => true, + }; + + // Check if all conditions match (AND condition) + let query_match = media_match && mq.expressions.iter().all(|expression| { + match expression { + &Width(value) => value.evaluate( + Au::from_frac_px(device.viewport_size.to_untyped().width as f64)), + } + }); + + // Apply the logical NOT qualifier to the result + match mq.qualifier { + Some(Not) => !query_match, + _ => query_match, } - // TODO: match Level 3 expressions }) } } + +#[cfg(test)] +mod tests { + use geom::size::TypedSize2D; + use properties::common_types::*; + use stylesheets::{iter_stylesheet_media_rules, iter_stylesheet_style_rules, Stylesheet}; + use super::*; + use url::Url; + + fn test_media_rule(css: &str, callback: |&MediaQueryList, &str|) { + let url = Url::parse("http://localhost").unwrap(); + let stylesheet = Stylesheet::from_str(css, url); + let mut rule_count: int = 0; + iter_stylesheet_media_rules(&stylesheet, |rule| { + rule_count += 1; + callback(&rule.media_queries, css); + }); + assert!(rule_count > 0); + } + + fn media_query_test(device: &Device, css: &str, expected_rule_count: int) { + let url = Url::parse("http://localhost").unwrap(); + let ss = Stylesheet::from_str(css, url); + let mut rule_count: int = 0; + iter_stylesheet_style_rules(&ss, device, |_| rule_count += 1); + assert!(rule_count == expected_rule_count, css.to_string()); + } + + #[test] + fn test_mq_empty() { + test_media_rule("@media { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_mq_screen() { + test_media_rule("@media screen { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == MediaType_(Screen), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media only screen { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Only), css.to_string()); + assert!(q.media_type == MediaType_(Screen), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media not screen { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == MediaType_(Screen), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_mq_print() { + test_media_rule("@media print { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == MediaType_(Print), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media only print { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Only), css.to_string()); + assert!(q.media_type == MediaType_(Print), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media not print { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == MediaType_(Print), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_mq_unknown() { + test_media_rule("@media fridge { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == MediaType_(Unknown), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media only glass { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Only), css.to_string()); + assert!(q.media_type == MediaType_(Unknown), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media not wood { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == MediaType_(Unknown), css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_mq_all() { + test_media_rule("@media all { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media only all { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Only), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media not all { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_mq_or() { + test_media_rule("@media screen, print { }", |list, css| { + assert!(list.media_queries.len() == 2, css.to_string()); + let q0 = &list.media_queries[0]; + assert!(q0.qualifier == None, css.to_string()); + assert!(q0.media_type == MediaType_(Screen), css.to_string()); + assert!(q0.expressions.len() == 0, css.to_string()); + + let q1 = &list.media_queries[1]; + assert!(q1.qualifier == None, css.to_string()); + assert!(q1.media_type == MediaType_(Print), css.to_string()); + assert!(q1.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_mq_default_expressions() { + test_media_rule("@media (min-width: 100px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 1, css.to_string()); + match q.expressions[0] { + Width(Min(w)) => assert!(w == Au::from_px(100)), + _ => fail!("wrong expression type"), + } + }); + + test_media_rule("@media (max-width: 43px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 1, css.to_string()); + match q.expressions[0] { + Width(Max(w)) => assert!(w == Au::from_px(43)), + _ => fail!("wrong expression type"), + } + }); + } + + #[test] + fn test_mq_expressions() { + test_media_rule("@media screen and (min-width: 100px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == MediaType_(Screen), css.to_string()); + assert!(q.expressions.len() == 1, css.to_string()); + match q.expressions[0] { + Width(Min(w)) => assert!(w == Au::from_px(100)), + _ => fail!("wrong expression type"), + } + }); + + test_media_rule("@media print and (max-width: 43px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == MediaType_(Print), css.to_string()); + assert!(q.expressions.len() == 1, css.to_string()); + match q.expressions[0] { + Width(Max(w)) => assert!(w == Au::from_px(43)), + _ => fail!("wrong expression type"), + } + }); + + test_media_rule("@media fridge and (max-width: 52px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == MediaType_(Unknown), css.to_string()); + assert!(q.expressions.len() == 1, css.to_string()); + match q.expressions[0] { + Width(Max(w)) => assert!(w == Au::from_px(52)), + _ => fail!("wrong expression type"), + } + }); + } + + #[test] + fn test_mq_multiple_expressions() { + test_media_rule("@media (min-width: 100px) and (max-width: 200px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == None, css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 2, css.to_string()); + match q.expressions[0] { + Width(Min(w)) => assert!(w == Au::from_px(100)), + _ => fail!("wrong expression type"), + } + match q.expressions[1] { + Width(Max(w)) => assert!(w == Au::from_px(200)), + _ => fail!("wrong expression type"), + } + }); + + test_media_rule("@media not screen and (min-width: 100px) and (max-width: 200px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == MediaType_(Screen), css.to_string()); + assert!(q.expressions.len() == 2, css.to_string()); + match q.expressions[0] { + Width(Min(w)) => assert!(w == Au::from_px(100)), + _ => fail!("wrong expression type"), + } + match q.expressions[1] { + Width(Max(w)) => assert!(w == Au::from_px(200)), + _ => fail!("wrong expression type"), + } + }); + } + + #[test] + fn test_mq_malformed_expressions() { + test_media_rule("@media (min-width: 100blah) and (max-width: 200px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media screen and (height: 200px) { }", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media (min-width: 30em foo bar) {}", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media not {}", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media not (min-width: 300px) {}", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media , {}", |list, css| { + assert!(list.media_queries.len() == 1, css.to_string()); + let q = &list.media_queries[0]; + assert!(q.qualifier == Some(Not), css.to_string()); + assert!(q.media_type == All, css.to_string()); + assert!(q.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media screen 4px, print {}", |list, css| { + assert!(list.media_queries.len() == 2, css.to_string()); + let q0 = &list.media_queries[0]; + assert!(q0.qualifier == Some(Not), css.to_string()); + assert!(q0.media_type == All, css.to_string()); + assert!(q0.expressions.len() == 0, css.to_string()); + let q1 = &list.media_queries[1]; + assert!(q1.qualifier == None, css.to_string()); + assert!(q1.media_type == MediaType_(Print), css.to_string()); + assert!(q1.expressions.len() == 0, css.to_string()); + }); + + test_media_rule("@media screen, {}", |list, css| { + assert!(list.media_queries.len() == 2, css.to_string()); + let q0 = &list.media_queries[0]; + assert!(q0.qualifier == None, css.to_string()); + assert!(q0.media_type == MediaType_(Screen), css.to_string()); + assert!(q0.expressions.len() == 0, css.to_string()); + let q1 = &list.media_queries[1]; + assert!(q1.qualifier == Some(Not), css.to_string()); + assert!(q1.media_type == All, css.to_string()); + assert!(q1.expressions.len() == 0, css.to_string()); + }); + } + + #[test] + fn test_matching_simple() { + let device = Device { + media_type: Screen, + viewport_size: TypedSize2D(200.0, 100.0), + }; + + media_query_test(&device, "@media not all { a { color: red; } }", 0); + media_query_test(&device, "@media not screen { a { color: red; } }", 0); + media_query_test(&device, "@media not print { a { color: red; } }", 1); + + media_query_test(&device, "@media unknown { a { color: red; } }", 0); + media_query_test(&device, "@media not unknown { a { color: red; } }", 1); + + media_query_test(&device, "@media { a { color: red; } }", 1); + media_query_test(&device, "@media screen { a { color: red; } }", 1); + media_query_test(&device, "@media print { a { color: red; } }", 0); + } + + #[test] + fn test_matching_width() { + let device = Device { + media_type: Screen, + viewport_size: TypedSize2D(200.0, 100.0), + }; + + media_query_test(&device, "@media { a { color: red; } }", 1); + + media_query_test(&device, "@media (min-width: 50px) { a { color: red; } }", 1); + media_query_test(&device, "@media (min-width: 150px) { a { color: red; } }", 1); + media_query_test(&device, "@media (min-width: 300px) { a { color: red; } }", 0); + + media_query_test(&device, "@media screen and (min-width: 50px) { a { color: red; } }", 1); + media_query_test(&device, "@media screen and (min-width: 150px) { a { color: red; } }", 1); + media_query_test(&device, "@media screen and (min-width: 300px) { a { color: red; } }", 0); + + media_query_test(&device, "@media not screen and (min-width: 50px) { a { color: red; } }", 0); + media_query_test(&device, "@media not screen and (min-width: 150px) { a { color: red; } }", 0); + media_query_test(&device, "@media not screen and (min-width: 300px) { a { color: red; } }", 1); + + media_query_test(&device, "@media (max-width: 50px) { a { color: red; } }", 0); + media_query_test(&device, "@media (max-width: 150px) { a { color: red; } }", 0); + media_query_test(&device, "@media (max-width: 300px) { a { color: red; } }", 1); + + media_query_test(&device, "@media screen and (min-width: 50px) and (max-width: 100px) { a { color: red; } }", 0); + media_query_test(&device, "@media screen and (min-width: 250px) and (max-width: 300px) { a { color: red; } }", 0); + media_query_test(&device, "@media screen and (min-width: 50px) and (max-width: 250px) { a { color: red; } }", 1); + + media_query_test(&device, "@media not screen and (min-width: 50px) and (max-width: 100px) { a { color: red; } }", 1); + media_query_test(&device, "@media not screen and (min-width: 250px) and (max-width: 300px) { a { color: red; } }", 1); + media_query_test(&device, "@media not screen and (min-width: 50px) and (max-width: 250px) { a { color: red; } }", 0); + + media_query_test(&device, "@media not screen and (min-width: 3.1em) and (max-width: 6em) { a { color: red; } }", 1); + media_query_test(&device, "@media not screen and (min-width: 16em) and (max-width: 19.75em) { a { color: red; } }", 1); + media_query_test(&device, "@media not screen and (min-width: 3em) and (max-width: 250px) { a { color: red; } }", 0); + } + + #[test] + fn test_matching_invalid() { + let device = Device { + media_type: Screen, + viewport_size: TypedSize2D(200.0, 100.0), + }; + + media_query_test(&device, "@media fridge { a { color: red; } }", 0); + media_query_test(&device, "@media screen and (height: 100px) { a { color: red; } }", 0); + media_query_test(&device, "@media not print and (width: 100) { a { color: red; } }", 0); + } +} diff --git a/servo/components/style/parsing_utils.rs b/servo/components/style/parsing_utils.rs index 1fd1034e117b..c2f6cc048505 100644 --- a/servo/components/style/parsing_utils.rs +++ b/servo/components/style/parsing_utils.rs @@ -42,6 +42,25 @@ impl> BufferedIter { assert!(self.buffer.is_none()); self.buffer = Some(value); } + + #[inline] + pub fn is_eof(&mut self) -> bool { + match self.next() { + Some(value) => { + self.push_back(value); + false + } + None => true + } + } + + #[inline] + pub fn next_as_result(&mut self) -> Result { + match self.next() { + Some(value) => Ok(value), + None => Err(()), + } + } } impl> Iterator for BufferedIter { diff --git a/servo/components/style/selector_matching.rs b/servo/components/style/selector_matching.rs index 2c5bc005b262..5ebef7c5e3d1 100644 --- a/servo/components/style/selector_matching.rs +++ b/servo/components/style/selector_matching.rs @@ -19,7 +19,7 @@ use servo_util::str::{AutoLpa, LengthLpa, PercentageLpa}; use string_cache::Atom; use legacy::{SizeIntegerAttribute, WidthLengthAttribute}; -use media_queries::{Device, Screen}; +use media_queries::Device; use node::{TElement, TElementAttributes, TNode}; use properties::{PropertyDeclaration, PropertyDeclarationBlock, SpecifiedValue, WidthDeclaration}; use properties::{specified}; @@ -272,7 +272,7 @@ pub struct Stylist { impl Stylist { #[inline] - pub fn new() -> Stylist { + pub fn new(device: &Device) -> Stylist { let mut stylist = Stylist { element_map: PerPseudoElementSelectorMap::new(), before_map: PerPseudoElementSelectorMap::new(), @@ -289,12 +289,13 @@ impl Stylist { Url::parse(format!("chrome:///{}", filename).as_slice()).unwrap(), None, None); - stylist.add_stylesheet(ua_stylesheet, UserAgentOrigin); + stylist.add_stylesheet(ua_stylesheet, UserAgentOrigin, device); } stylist } - pub fn add_stylesheet(&mut self, stylesheet: Stylesheet, origin: StylesheetOrigin) { + pub fn add_stylesheet(&mut self, stylesheet: Stylesheet, origin: StylesheetOrigin, + device: &Device) { let (mut element_map, mut before_map, mut after_map) = match origin { UserAgentOrigin => ( &mut self.element_map.user_agent, @@ -338,7 +339,6 @@ impl Stylist { }; ); - let device = &Device { media_type: Screen }; // TODO, use Print when printing iter_stylesheet_style_rules(&stylesheet, device, |style_rule| { append!(style_rule, normal); append!(style_rule, important); diff --git a/servo/components/style/stylesheets.rs b/servo/components/style/stylesheets.rs index cad9ea3b88d1..db1f13a82401 100644 --- a/servo/components/style/stylesheets.rs +++ b/servo/components/style/stylesheets.rs @@ -14,7 +14,7 @@ use selectors; use properties; use errors::{ErrorLoggerIterator, log_css_error}; use namespaces::{NamespaceMap, parse_namespace_rule}; -use media_queries::{MediaRule, parse_media_rule}; +use media_queries::{Device, MediaRule, parse_media_rule}; use media_queries; use font_face::{FontFaceRule, Source, parse_font_face_rule, iter_font_face_rules_inner}; @@ -165,6 +165,16 @@ pub fn iter_style_rules<'a>(rules: &[CSSRule], device: &media_queries::Device, } } +#[cfg(test)] +pub fn iter_stylesheet_media_rules(stylesheet: &Stylesheet, callback: |&MediaRule|) { + for rule in stylesheet.rules.iter() { + match *rule { + CSSMediaRule(ref rule) => callback(rule), + _ => {} + } + } +} + #[inline] pub fn iter_stylesheet_style_rules(stylesheet: &Stylesheet, device: &media_queries::Device, callback: |&StyleRule|) { @@ -173,6 +183,7 @@ pub fn iter_stylesheet_style_rules(stylesheet: &Stylesheet, device: &media_queri #[inline] -pub fn iter_font_face_rules(stylesheet: &Stylesheet, callback: |family: &str, source: &Source|) { - iter_font_face_rules_inner(stylesheet.rules.as_slice(), callback) +pub fn iter_font_face_rules(stylesheet: &Stylesheet, device: &Device, + callback: |family: &str, source: &Source|) { + iter_font_face_rules_inner(stylesheet.rules.as_slice(), device, callback) }