Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Srcset Attribute #18313

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

@@ -55,9 +55,28 @@ use std::cell::{Cell, RefMut};
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::str::is_ascii_digit;
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<u32>,
pub den: Option<f64>,
}

#[derive(Clone, Copy, HeapSizeOf, JSTraceable)]
#[allow(dead_code)]
enum State {
@@ -739,6 +758,11 @@ impl HTMLImageElementMethods for HTMLImageElement {
// https://html.spec.whatwg.org/multipage/#dom-img-src
make_setter!(SetSrc, "src");

// https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute
make_url_getter!(Srcset, "srcset");
// https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute
make_setter!(SetSrcset, "srcset");

// https://html.spec.whatwg.org/multipage/#dom-img-crossOrigin
fn GetCrossOrigin(&self) -> Option<DOMString> {
reflect_cross_origin_attribute(self.upcast::<Element>())
@@ -978,3 +1002,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<F>(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<ImageSource> {
let mut url_len = 0;
let mut candidates: Vec<ImageSource> = 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));

This comment has been minimized.

Copy link
@wafflespeanut

wafflespeanut Sep 24, 2017

Member

This is probably the only place where we're making use of idx - so, let's replace all instances of idx with _ in the patterns above.

continue;
}
None => {
if !current_descriptor.is_empty() {
descriptors.push(current_descriptor.clone());
}
break;
}
}
}
}
}

let mut error = false;
let mut width: Option<u32> = None;
let mut density: Option<f64> = None;
let mut future_compat_h: Option<u32> = 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
}
@@ -9,8 +9,8 @@ interface HTMLImageElement : HTMLElement {
attribute DOMString alt;
[CEReactions]
attribute DOMString src;
// [CEReactions]
// attribute DOMString srcset;
[CEReactions]

This comment has been minimized.

Copy link
@KiChjang

KiChjang Sep 5, 2017

Member

In order to generate appropriate bindings for the return type, you'll need to add the [Throws] attribute here.

attribute DOMString srcset;
[CEReactions]
attribute DOMString? crossOrigin;
[CEReactions]
@@ -58,3 +58,7 @@ pub mod size_of {
size_of::<Text>()
}
}

pub mod srcset {
pub use dom::htmlimageelement::{parse_a_srcset_attribute, ImageSource, Descriptor};
}
@@ -58,7 +58,8 @@ pub fn split_commas<'a>(s: &'a str) -> Filter<Split<'a, char>, 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,
@@ -0,0 +1,84 @@
/* 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 script::test::srcset::{Descriptor, ImageSource, parse_a_srcset_attribute};

#[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);
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.