diff --git a/components/script/dom/htmlimageelement.rs b/components/script/dom/htmlimageelement.rs index 1fea05fc0d9..027221a88f0 100644 --- a/components/script/dom/htmlimageelement.rs +++ b/components/script/dom/htmlimageelement.rs @@ -57,15 +57,34 @@ use std::char; use std::default::Default; use std::i32; use std::sync::{Arc, Mutex}; -use style::attr::{AttrValue, LengthOrPercentageOrAuto}; +use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_double, parse_unsigned_integer}; use style::context::QuirksMode; use style::media_queries::MediaQuery; use style::parser::ParserContext; +use style::str::is_ascii_digit; use style::values::specified::{Length, ViewportPercentageLength}; use style::values::specified::length::NoCalcLength; use style_traits::ParsingMode; use task_source::TaskSource; +enum ParseState { + InDescriptor, + InParens, + AfterDescriptor, +} + +#[derive(Debug, PartialEq)] +pub struct ImageSource { + pub url: String, + pub descriptor: Descriptor, +} + +#[derive(Debug, PartialEq)] +pub struct Descriptor { + pub wid: Option, + pub den: Option, +} + #[derive(Clone, Copy, HeapSizeOf, JSTraceable)] #[allow(dead_code)] enum State { @@ -1047,3 +1066,177 @@ fn image_dimension_setter(element: &Element, attr: LocalName, value: u32) { let value = AttrValue::Dimension(value.to_string(), dim); element.set_attribute(&attr, value); } + +/// Collect sequence of code points +pub fn collect_sequence_characters(s: &str, predicate: F) -> (&str, &str) + where F: Fn(&char) -> bool +{ + for (i, ch) in s.chars().enumerate() { + if !predicate(&ch) { + return (&s[0..i], &s[i..]) + } + } + + return (s, ""); +} + +/// Parse an `srcset` attribute - https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute. +pub fn parse_a_srcset_attribute(input: &str) -> Vec { + let mut url_len = 0; + let mut candidates: Vec = vec![]; + while url_len < input.len() { + let position = &input[url_len..]; + let (spaces, position) = collect_sequence_characters(position, |c| *c == ',' || char::is_whitespace(*c)); + // add the length of the url that we parse to advance the start index + let space_len = spaces.char_indices().count(); + url_len += space_len; + if position.is_empty() { + return candidates; + } + let (url, spaces) = collect_sequence_characters(position, |c| !char::is_whitespace(*c)); + // add the counts of urls that we parse to advance the start index + url_len += url.chars().count(); + let comma_count = url.chars().rev().take_while(|c| *c == ',').count(); + let url: String = url.chars().take(url.chars().count() - comma_count).collect(); + // add 1 to start index, for the comma + url_len += comma_count + 1; + let (space, position) = collect_sequence_characters(spaces, |c| char::is_whitespace(*c)); + let space_len = space.len(); + url_len += space_len; + let mut descriptors = Vec::new(); + let mut current_descriptor = String::new(); + let mut state = ParseState::InDescriptor; + let mut char_stream = position.chars().enumerate(); + let mut buffered: Option<(usize, char)> = None; + loop { + let next_char = buffered.take().or_else(|| char_stream.next()); + if next_char.is_some() { + url_len += 1; + } + match state { + ParseState::InDescriptor => { + match next_char { + Some((_, ' ')) => { + if !current_descriptor.is_empty() { + descriptors.push(current_descriptor.clone()); + current_descriptor = String::new(); + state = ParseState::AfterDescriptor; + } + continue; + } + Some((_, ',')) => { + if !current_descriptor.is_empty() { + descriptors.push(current_descriptor.clone()); + } + break; + } + Some((_, c @ '(')) => { + current_descriptor.push(c); + state = ParseState::InParens; + continue; + } + Some((_, c)) => { + current_descriptor.push(c); + } + None => { + if !current_descriptor.is_empty() { + descriptors.push(current_descriptor.clone()); + } + break; + } + } + } + ParseState::InParens => { + match next_char { + Some((_, c @ ')')) => { + current_descriptor.push(c); + state = ParseState::InDescriptor; + continue; + } + Some((_, c)) => { + current_descriptor.push(c); + continue; + } + None => { + if !current_descriptor.is_empty() { + descriptors.push(current_descriptor.clone()); + } + break; + } + } + } + ParseState::AfterDescriptor => { + match next_char { + Some((_, ' ')) => { + state = ParseState::AfterDescriptor; + continue; + } + Some((idx, c)) => { + state = ParseState::InDescriptor; + buffered = Some((idx, c)); + continue; + } + None => { + if !current_descriptor.is_empty() { + descriptors.push(current_descriptor.clone()); + } + break; + } + } + } + } + } + + let mut error = false; + let mut width: Option = None; + let mut density: Option = None; + let mut future_compat_h: Option = None; + for descriptor in descriptors { + let (digits, remaining) = collect_sequence_characters(&descriptor, |c| is_ascii_digit(c) || *c == '.'); + let valid_non_negative_integer = parse_unsigned_integer(digits.chars()); + let has_w = remaining == "w"; + let valid_floating_point = parse_double(digits); + let has_x = remaining == "x"; + let has_h = remaining == "h"; + if valid_non_negative_integer.is_ok() && has_w { + let result = valid_non_negative_integer; + error = result.is_err(); + if width.is_some() || density.is_some() { + error = true; + } + if let Ok(w) = result { + width = Some(w); + } + } else if valid_floating_point.is_ok() && has_x { + let result = valid_floating_point; + error = result.is_err(); + if width.is_some() || density.is_some() || future_compat_h.is_some() { + error = true; + } + if let Ok(x) = result { + density = Some(x); + } + } else if valid_non_negative_integer.is_ok() && has_h { + let result = valid_non_negative_integer; + error = result.is_err(); + if density.is_some() || future_compat_h.is_some() { + error = true; + } + if let Ok(h) = result { + future_compat_h = Some(h); + } + } else { + error = true; + } + } + if future_compat_h.is_some() && width.is_none() { + error = true; + } + if !error { + let descriptor = Descriptor { wid: width, den: density }; + let image_source = ImageSource { url: url, descriptor: descriptor }; + candidates.push(image_source); + } + } + candidates +} diff --git a/components/script/test.rs b/components/script/test.rs index 88baa21f42f..351c3447688 100644 --- a/components/script/test.rs +++ b/components/script/test.rs @@ -62,3 +62,7 @@ pub mod size_of { size_of::() } } + +pub mod srcset { + pub use dom::htmlimageelement::{parse_a_srcset_attribute, ImageSource, Descriptor}; +} diff --git a/components/style/str.rs b/components/style/str.rs index a76bf98caf5..bff3f7b43cf 100644 --- a/components/style/str.rs +++ b/components/style/str.rs @@ -58,7 +58,8 @@ pub fn split_commas<'a>(s: &'a str) -> Filter, fn(&&str) -> bool s.split(',').filter(not_empty as fn(&&str) -> bool) } -fn is_ascii_digit(c: &char) -> bool { +/// Character is ascii digit +pub fn is_ascii_digit(c: &char) -> bool { match *c { '0'...'9' => true, _ => false, diff --git a/tests/unit/script/htmlimageelement.rs b/tests/unit/script/htmlimageelement.rs index 5c9106b6c0f..6d62f25a500 100644 --- a/tests/unit/script/htmlimageelement.rs +++ b/tests/unit/script/htmlimageelement.rs @@ -4,6 +4,7 @@ use script::test::DOMString; use script::test::sizes::{parse_a_sizes_attribute, Size}; +use script::test::srcset::{Descriptor, ImageSource, parse_a_srcset_attribute}; use style::media_queries::{MediaQuery, MediaQueryType}; use style::media_queries::Expression; use style::servo::media_queries::{ExpressionKind, Range}; @@ -99,3 +100,82 @@ fn extra_whitespace() { DOMString::from("(max-width: 900px) 1000px, (max-width: 900px) 50px"), None), a); } + +#[test] +fn no_value() { + let new_vec = Vec::new(); + assert_eq!(parse_a_srcset_attribute(" "), new_vec); +} + +#[test] +fn width_one_value() { + let first_descriptor = Descriptor { wid: Some(320), den: None }; + let first_imagesource = ImageSource { url: "small-image.jpg".to_string(), descriptor: first_descriptor }; + let sources = &[first_imagesource]; + assert_eq!(parse_a_srcset_attribute("small-image.jpg, 320w"), sources); +} + +#[test] +fn width_two_value() { + let first_descriptor = Descriptor { wid: Some(320), den: None }; + let first_imagesource = ImageSource { url: "small-image.jpg".to_string(), descriptor: first_descriptor }; + let second_descriptor = Descriptor { wid: Some(480), den: None }; + let second_imagesource = ImageSource { url: "medium-image.jpg".to_string(), descriptor: second_descriptor }; + let sources = &[first_imagesource, second_imagesource]; + assert_eq!(parse_a_srcset_attribute("small-image.jpg 320w, medium-image.jpg 480w"), sources); +} + +#[test] +fn width_three_value() { + let first_descriptor = Descriptor { wid: Some(320), den: None }; + let first_imagesource = ImageSource { url: "smallImage.jpg".to_string(), descriptor: first_descriptor }; + let second_descriptor = Descriptor { wid: Some(480), den: None }; + let second_imagesource = ImageSource { url: "mediumImage.jpg".to_string(), descriptor: second_descriptor }; + let third_descriptor = Descriptor { wid: Some(800), den: None }; + let third_imagesource = ImageSource { url: "largeImage.jpg".to_string(), descriptor: third_descriptor }; + let sources = &[first_imagesource, second_imagesource, third_imagesource]; + assert_eq!(parse_a_srcset_attribute("smallImage.jpg 320w, + mediumImage.jpg 480w, + largeImage.jpg 800w"), sources); +} + +#[test] +fn density_value() { + let first_descriptor = Descriptor { wid: None, den: Some(1.0) }; + let first_imagesource = ImageSource { url: "small-image.jpg".to_string(), descriptor: first_descriptor }; + let sources = &[first_imagesource]; + assert_eq!(parse_a_srcset_attribute("small-image.jpg 1x"), sources); +} + +#[test] +fn without_descriptor() { + let first_descriptor = Descriptor { wid: None, den: None }; + let first_imagesource = ImageSource { url: "small-image.jpg".to_string(), descriptor: first_descriptor }; + let sources = &[first_imagesource]; + assert_eq!(parse_a_srcset_attribute("small-image.jpg"), sources); +} + +//Does not parse an ImageSource when both width and density descriptor present +#[test] +fn two_descriptor() { + let empty_vec = Vec::new(); + assert_eq!(parse_a_srcset_attribute("small-image.jpg 320w 1.1x"), empty_vec); +} + +#[test] +fn decimal_descriptor() { + let first_descriptor = Descriptor { wid: None, den: Some(2.2) }; + let first_imagesource = ImageSource { url: "small-image.jpg".to_string(), descriptor: first_descriptor }; + let sources = &[first_imagesource]; + assert_eq!(parse_a_srcset_attribute("small-image.jpg 2.2x"), sources); +} + +#[test] +fn different_descriptor() { + let first_descriptor = Descriptor { wid: Some(320), den: None }; + let first_imagesource = ImageSource { url: "small-image.jpg".to_string(), descriptor: first_descriptor }; + let second_descriptor = Descriptor { wid: None, den: Some(2.2) }; + let second_imagesource = ImageSource { url: "medium-image.jpg".to_string(), descriptor: second_descriptor }; + let sources = &[first_imagesource, second_imagesource]; + assert_eq!(parse_a_srcset_attribute("small-image.jpg 320w, medium-image.jpg 2.2x"), sources); +}