Skip to main content

script/dom/
srcset.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::sync::LazyLock;
6
7use app_units::Au;
8use cssparser::Parser;
9use regex::Regex;
10use rustc_hash::FxHashSet;
11use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
12use script_bindings::inheritance::Castable;
13use script_bindings::root::DomRoot;
14use script_bindings::str::USVString;
15use style::attr::parse_unsigned_integer;
16use style::stylesheets::CssRuleType;
17use style::values::specified::source_size_list::SourceSizeList;
18use style_traits::ParsingMode;
19use xml5ever::local_name;
20
21use crate::css::css::{ANONYMOUS_CONTENT_URL_DATA, parser_context_for_anonymous_content};
22use crate::dom::htmlimageelement::HTMLImageElement;
23use crate::dom::htmllinkelement::HTMLLinkElement;
24use crate::dom::htmlpictureelement::HTMLPictureElement;
25use crate::dom::htmlsourceelement::HTMLSourceElement;
26use crate::dom::medialist::MediaList;
27use crate::dom::node::NodeTraits;
28use crate::dom::{Document, Element, Node};
29
30/// Supported image MIME types as defined by
31/// <https://mimesniff.spec.whatwg.org/#image-mime-type>.
32/// Keep this in sync with 'detect_image_format' from components/pixels/lib.rs
33const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &[
34    "image/bmp",
35    "image/gif",
36    "image/jpeg",
37    "image/jpg",
38    "image/pjpeg",
39    "image/png",
40    "image/apng",
41    "image/x-png",
42    "image/svg+xml",
43    "image/vnd.microsoft.icon",
44    "image/x-icon",
45    "image/webp",
46];
47
48/// <https://html.spec.whatwg.org/multipage/#source-set>
49#[derive(Clone, Debug, MallocSizeOf)]
50pub(crate) struct SourceSet {
51    pub image_sources: Vec<ImageSource>,
52    pub source_size: SourceSizeList,
53}
54
55/// <https://html.spec.whatwg.org/multipage/#image-source>
56#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
57pub struct ImageSource {
58    pub url: String,
59    pub descriptor: Descriptor,
60}
61
62/// <https://html.spec.whatwg.org/multipage/#width-descriptor>
63/// <https://html.spec.whatwg.org/multipage/#pixel-density-descriptor>
64#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
65pub struct Descriptor {
66    pub width: Option<u32>,
67    pub density: Option<f64>,
68}
69
70#[derive(Clone, Copy, Debug)]
71enum ParseState {
72    InDescriptor,
73    InParens,
74    AfterDescriptor,
75}
76
77impl SourceSet {
78    pub fn new() -> SourceSet {
79        SourceSet {
80            image_sources: Vec::new(),
81            source_size: SourceSizeList::empty(),
82        }
83    }
84
85    /// <https://html.spec.whatwg.org/multipage/#create-a-source-set>
86    pub fn create_source_set(
87        default_source: &str,
88        srcset: &str,
89        sizes: &str,
90        document: &Document,
91    ) -> SourceSet {
92        // Step 1. Let source set be an empty source set.
93        let mut source_set = SourceSet::new();
94
95        // Step 2. If srcset is not an empty string, then set source set to the result of parsing
96        // srcset.
97        if !srcset.is_empty() {
98            source_set.image_sources = parse_a_srcset_attribute(srcset);
99        }
100
101        // Step 3. Set source set's source size to the result of parsing sizes with img.
102        if !sizes.is_empty() {
103            source_set.source_size = parse_a_sizes_attribute(sizes);
104        }
105
106        // Step 4. If default source is not the empty string and source set does not contain an
107        // image source with a pixel density descriptor value of 1, and no image source with a width
108        // descriptor, append default source to source set.
109        let no_density_source_of_1 = source_set
110            .image_sources
111            .iter()
112            .all(|source| source.descriptor.density != Some(1.));
113        let no_width_descriptor = source_set
114            .image_sources
115            .iter()
116            .all(|source| source.descriptor.width.is_none());
117        if !default_source.is_empty() && no_density_source_of_1 && no_width_descriptor {
118            source_set.image_sources.push(ImageSource {
119                url: String::from(default_source),
120                descriptor: Descriptor {
121                    width: None,
122                    density: None,
123                },
124            })
125        }
126
127        // Step 5. Normalize the source densities of source set.
128        source_set.normalise_source_densities(document);
129
130        // Step 6. Return source set.
131        source_set
132    }
133
134    /// <https://html.spec.whatwg.org/multipage/#update-the-source-set>
135    pub fn update_source_set(&mut self, el: &Element) {
136        // Step 1. Set el's source set to an empty source set.
137        *self = SourceSet::new();
138
139        // Step 2. Let elements be « el ».
140        // Step 3. If el is an img element whose parent node is a picture element, then replace the
141        // contents of elements with el's parent node's child elements, retaining relative order.
142        // Step 4. Let img be el if el is an img element, otherwise null.
143        let img = el.downcast::<HTMLImageElement>();
144        let parent = el.upcast::<Node>().GetParentElement();
145        let elements = match parent.as_ref() {
146            Some(p) => {
147                if p.is::<HTMLPictureElement>() {
148                    p.upcast::<Node>()
149                        .children()
150                        .filter_map(DomRoot::downcast::<Element>)
151                        .map(|n| DomRoot::from_ref(&*n))
152                        .collect()
153                } else {
154                    vec![DomRoot::from_ref(el)]
155                }
156            },
157            None => vec![DomRoot::from_ref(el)],
158        };
159
160        // Step 5. For each child in elements:
161        for child in &elements {
162            // Step 5.1. If child is el:
163            if *child == DomRoot::from_ref(el) {
164                let (default_source, srcset, sizes) = if el.is::<HTMLImageElement>() {
165                    // Step 5.1.4: If el is an img element that has a srcset attribute, then
166                    // set srcset to that attribute's value.
167                    let srcset = el
168                        .get_attribute_string_value(&local_name!("srcset"))
169                        .unwrap_or_default();
170                    // Step 5.1.6: If el is an img element that has a sizes attribute, then set sizes to that attribute's value.
171                    let sizes = el
172                        .get_attribute_string_value(&local_name!("sizes"))
173                        .unwrap_or_default();
174                    // Step 5.1.8: If el is an img element that has a src attribute, then set default source to that attribute's value.
175                    let default_source = el
176                        .get_attribute_string_value(&local_name!("src"))
177                        .unwrap_or_default();
178                    (default_source, srcset, sizes)
179                } else if el.is::<HTMLLinkElement>() {
180                    // Step 5.1.5: Otherwise, if el is a link element that has an imagesrcset attribute, then set srcset to that attribute's value.
181                    let srcset = el
182                        .get_attribute_string_value(&local_name!("imagesrcset"))
183                        .unwrap_or_default();
184                    // Step 5.1.7: Otherwise, if el is a link element that has an imagesizes attribute, then set sizes to that attribute's value.
185                    let sizes = el
186                        .get_attribute_string_value(&local_name!("imagesizes"))
187                        .unwrap_or_default();
188                    // Step 5.1.9: Otherwise, if el is a link element that has an href attribute, then set default source to that attribute's value.
189                    let default_source = el
190                        .get_attribute_string_value(&local_name!("href"))
191                        .unwrap_or_default();
192                    (default_source, srcset, sizes)
193                } else {
194                    // Step 5.1.1: Let default source be the empty string.
195                    // Step 5.1.2: Let srcset be the empty string.
196                    // Step 5.1.3: Let sizes be the empty string.
197                    (String::new(), String::new(), String::new())
198                };
199
200                // Step 5.1.10. Set el's source set to the result of creating a source set given
201                // default source, srcset, sizes, and img.
202                *self = SourceSet::create_source_set(
203                    &default_source,
204                    &srcset,
205                    &sizes,
206                    &el.owner_document(),
207                );
208
209                // Step 5.1.11. Return.
210                return;
211            }
212            // Spec note: If el is a link element, then elements contains only el, so this step
213            // will be reached immediately and the rest of the algorithm will not run.
214            debug_assert!(!el.is::<HTMLLinkElement>());
215            // Step 5.2. If child is not a source element, then continue.
216            if !child.is::<HTMLSourceElement>() {
217                continue;
218            }
219
220            let mut source_set = SourceSet::new();
221
222            // Step 5.3. If child does not have a srcset attribute, continue to the next child.
223            // Step 5.4. Parse child's srcset attribute and let source set be the returned source
224            // set.
225            match child.get_attribute_string_value(&local_name!("srcset")) {
226                Some(srcset) => {
227                    source_set.image_sources = parse_a_srcset_attribute(&srcset);
228                },
229                _ => continue,
230            }
231
232            // Step 5.5. If source set has zero image sources, continue to the next child.
233            if source_set.image_sources.is_empty() {
234                continue;
235            }
236
237            // Step 5.6. If child has a media attribute, and its value does not match the
238            // environment, continue to the next child.
239            if let Some(media) = child.get_attribute_string_value(&local_name!("media")) &&
240                !MediaList::matches_environment(&child.owner_document(), &media)
241            {
242                continue;
243            }
244
245            // Step 5.7. Parse child's sizes attribute with img, and let source set's source size be
246            // the returned value.
247            if let Some(sizes) = child.get_attribute_string_value(&local_name!("sizes")) {
248                source_set.source_size = parse_a_sizes_attribute(&sizes);
249            }
250
251            // Step 5.8. If child has a type attribute, and its value is an unknown or unsupported
252            // MIME type, continue to the next child.
253            if let Some(type_) = child.get_attribute_string_value(&local_name!("type")) &&
254                !is_supported_image_mime_type(&type_)
255            {
256                continue;
257            }
258
259            // Step 5.9. If child has width or height attributes, set el's dimension attribute
260            // source to child. Otherwise, set el's dimension attribute source to el.
261            if let Some(image) = img {
262                if child.has_attribute(&local_name!("width")) ||
263                    child.has_attribute(&local_name!("height"))
264                {
265                    image.set_dimension_attribute_source(Some(child));
266                } else {
267                    image.set_dimension_attribute_source(Some(el));
268                }
269            }
270
271            // Step 5.10. Normalize the source densities of source set.
272            source_set.normalise_source_densities(&el.owner_document());
273
274            // Step 5.11. Set el's source set to source set.
275            *self = source_set;
276
277            // Step 5.12. Return.
278            return;
279        }
280    }
281
282    pub fn evaluate_source_size_list(&self, document: &Document) -> Au {
283        let quirks_mode = document.quirks_mode();
284        self.source_size
285            .evaluate(document.window().layout().device(), quirks_mode)
286    }
287
288    /// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities>
289    pub fn normalise_source_densities(&mut self, document: &Document) {
290        // Step 1. Let source size be source set's source size.
291        let source_size = self.evaluate_source_size_list(document);
292
293        // Step 2. For each image source in source set:
294        for image_source in self.image_sources.iter_mut() {
295            // Step 2.1. If the image source has a pixel density descriptor, continue to the next
296            // image source.
297            if image_source.descriptor.density.is_some() {
298                continue;
299            }
300
301            // Step 2.2. Otherwise, if the image source has a width descriptor, replace the width
302            // descriptor with a pixel density descriptor with a value of the width descriptor value
303            // divided by source size and a unit of x.
304            if let Some(width) = image_source.descriptor.width {
305                image_source.descriptor.density = Some(width as f64 / source_size.to_f64_px());
306            } else {
307                // Step 2.3. Otherwise, give the image source a pixel density descriptor of 1x.
308                image_source.descriptor.density = Some(1_f64);
309            }
310        }
311    }
312
313    /// <https://html.spec.whatwg.org/multipage/#select-an-image-source>
314    pub fn select_image_source(&mut self, element: &Element) -> Option<(USVString, f64)> {
315        // Step 1. Update the source set for el.
316        self.update_source_set(element);
317
318        // Step 2. If el's source set is empty, return null as the URL and undefined as the pixel
319        // density.
320        if self.image_sources.is_empty() {
321            return None;
322        }
323
324        // Step 3. Return the result of selecting an image from el's source set.
325        self.select_image_source_from_source_set(&element.owner_document())
326    }
327
328    /// <https://html.spec.whatwg.org/multipage/#select-an-image-source-from-a-source-set>
329    pub fn select_image_source_from_source_set(
330        &self,
331        document: &Document,
332    ) -> Option<(USVString, f64)> {
333        // Step 1. If an entry b in sourceSet has the same associated pixel density descriptor as an
334        // earlier entry a in sourceSet, then remove entry b. Repeat this step until none of the
335        // entries in sourceSet have the same associated pixel density descriptor as an earlier
336        // entry.
337        let len = self.image_sources.len();
338
339        // Using FxHash is ok here as the indices are just 0..len
340        let mut repeat_indices = FxHashSet::default();
341        for outer_index in 0..len {
342            if repeat_indices.contains(&outer_index) {
343                continue;
344            }
345            let imgsource = &self.image_sources[outer_index];
346            let pixel_density = imgsource.descriptor.density.unwrap();
347            for inner_index in (outer_index + 1)..len {
348                let imgsource2 = &self.image_sources[inner_index];
349                if pixel_density == imgsource2.descriptor.density.unwrap() {
350                    repeat_indices.insert(inner_index);
351                }
352            }
353        }
354
355        let mut max = (0f64, 0);
356        let img_sources = &mut vec![];
357        for (index, image_source) in self.image_sources.iter().enumerate() {
358            if repeat_indices.contains(&index) {
359                continue;
360            }
361            let den = image_source.descriptor.density.unwrap();
362            if max.0 < den {
363                max = (den, img_sources.len());
364            }
365            img_sources.push(image_source);
366        }
367
368        // Step 2. In an implementation-defined manner, choose one image source from sourceSet. Let
369        // selectedSource be this choice.
370        let mut best_candidate = max;
371        let device_pixel_ratio = document
372            .window()
373            .viewport_details()
374            .hidpi_scale_factor
375            .get() as f64;
376        for (index, image_source) in img_sources.iter().enumerate() {
377            let current_den = image_source.descriptor.density.unwrap();
378            if current_den < best_candidate.0 && current_den >= device_pixel_ratio {
379                best_candidate = (current_den, index);
380            }
381        }
382        let selected_source = img_sources.remove(best_candidate.1).clone();
383
384        // Step 3. Return selectedSource and its associated pixel density.
385        Some((
386            USVString(selected_source.url),
387            selected_source.descriptor.density.unwrap(),
388        ))
389    }
390}
391
392/// <https://html.spec.whatwg.org/multipage/#parse-a-sizes-attribute>
393pub fn parse_a_sizes_attribute(value: &str) -> SourceSizeList {
394    let mut parser = Parser::new(value);
395    // FIXME(emilio): why ::empty() instead of ::DEFAULT? Also, what do
396    // browsers do regarding quirks-mode in a media list?
397    let context = parser_context_for_anonymous_content(
398        CssRuleType::Style,
399        ParsingMode::empty(),
400        &ANONYMOUS_CONTENT_URL_DATA,
401    );
402    SourceSizeList::parse(&context, &mut parser)
403}
404
405/// Collect sequence of code points
406/// <https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points>
407pub(crate) fn collect_sequence_characters(
408    s: &str,
409    mut predicate: impl FnMut(&char) -> bool,
410) -> (&str, &str) {
411    let i = s.find(|ch| !predicate(&ch)).unwrap_or(s.len());
412    (&s[0..i], &s[i..])
413}
414
415/// <https://html.spec.whatwg.org/multipage/#valid-non-negative-integer>
416/// TODO(#39315): Use the validation rule from Stylo
417fn is_valid_non_negative_integer_string(s: &str) -> bool {
418    s.chars().all(|c| c.is_ascii_digit())
419}
420
421/// <https://html.spec.whatwg.org/multipage/#valid-floating-point-number>
422/// TODO(#39315): Use the validation rule from Stylo
423fn is_valid_floating_point_number_string(s: &str) -> bool {
424    static RE: LazyLock<Regex> =
425        LazyLock::new(|| Regex::new(r"^-?(?:\d+\.\d+|\d+|\.\d+)(?:(e|E)(\+|\-)?\d+)?$").unwrap());
426
427    RE.is_match(s)
428}
429
430/// Parse an `srcset` attribute:
431/// <https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute>.
432pub fn parse_a_srcset_attribute(input: &str) -> Vec<ImageSource> {
433    // > 1. Let input be the value passed to this algorithm.
434    // > 2. Let position be a pointer into input, initially pointing at the start of the string.
435    let mut current_index = 0;
436
437    // > 3. Let candidates be an initially empty source set.
438    let mut candidates = vec![];
439    while current_index < input.len() {
440        let remaining_string = &input[current_index..];
441
442        // > 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or
443        // > U+002C COMMA characters from input given position. If any U+002C COMMA
444        // > characters were collected, that is a parse error.
445        // NOTE: A parse error indicating a non-fatal mismatch between the input and the
446        // requirements will be silently ignored to match the behavior of other browsers.
447        // <https://html.spec.whatwg.org/multipage/#concept-microsyntax-parse-error>
448        let (collected_characters, string_after_whitespace) =
449            collect_sequence_characters(remaining_string, |character| {
450                *character == ',' || character.is_ascii_whitespace()
451            });
452
453        // Add the length of collected whitespace, to find the start of the URL we are going
454        // to parse.
455        current_index += collected_characters.len();
456
457        // > 5. If position is past the end of input, return candidates.
458        if string_after_whitespace.is_empty() {
459            return candidates;
460        }
461
462        // 6. Collect a sequence of code points that are not ASCII whitespace from input
463        // given position, and let that be url.
464        let (url, _) =
465            collect_sequence_characters(string_after_whitespace, |c| !char::is_ascii_whitespace(c));
466
467        // Add the length of `url` that we will parse to advance the index of the next part
468        // of the string to prase.
469        current_index += url.len();
470
471        // 7. Let descriptors be a new empty list.
472        let mut descriptors = Vec::new();
473
474        // > 8. If url ends with U+002C (,), then:
475        // >    1. Remove all trailing U+002C COMMA characters from url. If this removed
476        // >       more than one character, that is a parse error.
477        if url.ends_with(',') {
478            let image_source = ImageSource {
479                url: url.trim_end_matches(',').into(),
480                descriptor: Descriptor {
481                    width: None,
482                    density: None,
483                },
484            };
485            candidates.push(image_source);
486            continue;
487        }
488
489        // Otherwise:
490        // > 8.1. Descriptor tokenizer: Skip ASCII whitespace within input given position.
491        let descriptors_string = &input[current_index..];
492        let (spaces, descriptors_string) =
493            collect_sequence_characters(descriptors_string, |character| {
494                character.is_ascii_whitespace()
495            });
496        current_index += spaces.len();
497
498        // > 8.2. Let current descriptor be the empty string.
499        let mut current_descriptor = String::new();
500
501        // > 8.3. Let state be "in descriptor".
502        let mut state = ParseState::InDescriptor;
503
504        // > 8.4. Let c be the character at position. Do the following depending on the value of
505        // > state. For the purpose of this step, "EOF" is a special character representing
506        // > that position is past the end of input.
507        let mut characters = descriptors_string.chars();
508        let mut character = characters.next();
509        if let Some(character) = character {
510            current_index += character.len_utf8();
511        }
512
513        loop {
514            match (state, character) {
515                (ParseState::InDescriptor, Some(character)) if character.is_ascii_whitespace() => {
516                    // > If current descriptor is not empty, append current descriptor to
517                    // > descriptors and let current descriptor be the empty string. Set
518                    // > state to after descriptor.
519                    if !current_descriptor.is_empty() {
520                        descriptors.push(current_descriptor);
521                        current_descriptor = String::new();
522                        state = ParseState::AfterDescriptor;
523                    }
524                },
525                (ParseState::InDescriptor, Some(',')) => {
526                    // > Advance position to the next character in input. If current descriptor
527                    // > is not empty, append current descriptor to descriptors. Jump to the
528                    // > step labeled descriptor parser.
529                    if !current_descriptor.is_empty() {
530                        descriptors.push(current_descriptor);
531                    }
532                    break;
533                },
534                (ParseState::InDescriptor, Some('(')) => {
535                    // > Append c to current descriptor. Set state to in parens.
536                    current_descriptor.push('(');
537                    state = ParseState::InParens;
538                },
539                (ParseState::InDescriptor, Some(character)) => {
540                    // > Append c to current descriptor.
541                    current_descriptor.push(character);
542                },
543                (ParseState::InDescriptor, None) => {
544                    // > If current descriptor is not empty, append current descriptor to
545                    // > descriptors. Jump to the step labeled descriptor parser.
546                    if !current_descriptor.is_empty() {
547                        descriptors.push(current_descriptor);
548                    }
549                    break;
550                },
551                (ParseState::InParens, Some(')')) => {
552                    // > Append c to current descriptor. Set state to in descriptor.
553                    current_descriptor.push(')');
554                    state = ParseState::InDescriptor;
555                },
556                (ParseState::InParens, Some(character)) => {
557                    // Append c to current descriptor.
558                    current_descriptor.push(character);
559                },
560                (ParseState::InParens, None) => {
561                    // > Append current descriptor to descriptors. Jump to the step
562                    // > labeled descriptor parser.
563                    descriptors.push(current_descriptor);
564                    break;
565                },
566                (ParseState::AfterDescriptor, Some(character))
567                    if character.is_ascii_whitespace() =>
568                {
569                    // > Stay in this state.
570                },
571                (ParseState::AfterDescriptor, Some(_)) => {
572                    // > Set state to in descriptor. Set position to the previous
573                    // > character in input.
574                    state = ParseState::InDescriptor;
575                    continue;
576                },
577                (ParseState::AfterDescriptor, None) => {
578                    // > Jump to the step labeled descriptor parser.
579                    break;
580                },
581            }
582
583            character = characters.next();
584            if let Some(character) = character {
585                current_index += character.len_utf8();
586            }
587        }
588
589        // > 9. Descriptor parser: Let error be no.
590        let mut error = false;
591        // > 10. Let width be absent.
592        let mut width: Option<u32> = None;
593        // > 11. Let density be absent.
594        let mut density: Option<f64> = None;
595        // > 12. Let future-compat-h be absent.
596        let mut future_compat_h: Option<u32> = None;
597
598        // > 13. For each descriptor in descriptors, run the appropriate set of steps from
599        // > the following list:
600        for descriptor in descriptors.into_iter() {
601            let Some(last_character) = descriptor.chars().last() else {
602                break;
603            };
604
605            let first_part_of_string = &descriptor[0..descriptor.len() - last_character.len_utf8()];
606            match last_character {
607                // > If the descriptor consists of a valid non-negative integer followed by a
608                // > U+0077 LATIN SMALL LETTER W character
609                // > 1. If the user agent does not support the sizes attribute, let error be yes.
610                // > 2. If width and density are not both absent, then let error be yes.
611                // > 3. Apply the rules for parsing non-negative integers to the descriptor.
612                // >    If the result is 0, let error be yes. Otherwise, let width be the result.
613                'w' if is_valid_non_negative_integer_string(first_part_of_string) &&
614                    density.is_none() &&
615                    width.is_none() =>
616                {
617                    match parse_unsigned_integer(first_part_of_string.chars()) {
618                        Ok(number) if number > 0 => {
619                            width = Some(number);
620                            continue;
621                        },
622                        _ => error = true,
623                    }
624                },
625
626                // > If the descriptor consists of a valid floating-point number followed by a
627                // > U+0078 LATIN SMALL LETTER X character
628                // > 1. If width, density and future-compat-h are not all absent, then let
629                // >    error be yes.
630                // > 2. Apply the rules for parsing floating-point number values to the
631                // >    descriptor. If the result is less than 0, let error be yes. Otherwise, let
632                // >    density be the result.
633                //
634                // The HTML specification has a procedure for parsing floats that is different enough from
635                // the one that stylo uses, that it's better to use Rust's float parser here. This is
636                // what Gecko does, but it also checks to see if the number is a valid HTML-spec compliant
637                // number first. Not doing that means that we might be parsing numbers that otherwise
638                // wouldn't parse.
639                'x' if is_valid_floating_point_number_string(first_part_of_string) &&
640                    width.is_none() &&
641                    density.is_none() &&
642                    future_compat_h.is_none() =>
643                {
644                    match first_part_of_string.parse::<f64>() {
645                        Ok(number) if number.is_finite() && number >= 0. => {
646                            density = Some(number);
647                            continue;
648                        },
649                        _ => error = true,
650                    }
651                },
652
653                // > If the descriptor consists of a valid non-negative integer followed by a
654                // > U+0068 LATIN SMALL LETTER H character
655                // >   This is a parse error.
656                // > 1. If future-compat-h and density are not both absent, then let error be
657                // >    yes.
658                // > 2. Apply the rules for parsing non-negative integers to the descriptor.
659                // >    If the result is 0, let error be yes. Otherwise, let future-compat-h be the
660                // >    result.
661                'h' if is_valid_non_negative_integer_string(first_part_of_string) &&
662                    future_compat_h.is_none() &&
663                    density.is_none() =>
664                {
665                    match parse_unsigned_integer(first_part_of_string.chars()) {
666                        Ok(number) if number > 0 => {
667                            future_compat_h = Some(number);
668                            continue;
669                        },
670                        _ => error = true,
671                    }
672                },
673
674                // > Anything else
675                // >  Let error be yes.
676                _ => error = true,
677            }
678
679            if error {
680                break;
681            }
682        }
683
684        // > 14. If future-compat-h is not absent and width is absent, let error be yes.
685        if future_compat_h.is_some() && width.is_none() {
686            error = true;
687        }
688
689        // Step 15. If error is still no, then append a new image source to candidates whose URL is
690        // url, associated with a width width if not absent and a pixel density density if not
691        // absent. Otherwise, there is a parse error.
692        if !error {
693            let image_source = ImageSource {
694                url: url.into(),
695                descriptor: Descriptor { width, density },
696            };
697            candidates.push(image_source);
698        }
699
700        // Step 16. Return to the step labeled splitting loop.
701    }
702    candidates
703}
704
705/// Returns true if the given image MIME type is supported.
706fn is_supported_image_mime_type(input: &str) -> bool {
707    // Remove any leading and trailing HTTP whitespace from input.
708    let mime_type = input.trim();
709
710    // <https://mimesniff.spec.whatwg.org/#mime-type-essence>
711    let mime_type_essence = match mime_type.find(';') {
712        Some(semi) => &mime_type[..semi],
713        _ => mime_type,
714    };
715
716    // The HTML specification says the type attribute may be present and if present, the value
717    // must be a valid MIME type string. However an empty type attribute is implicitly supported
718    // to match the behavior of other browsers.
719    // <https://html.spec.whatwg.org/multipage/#attr-source-type>
720    if mime_type_essence.is_empty() {
721        return true;
722    }
723
724    SUPPORTED_IMAGE_MIME_TYPES.contains(&mime_type_essence)
725}