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