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, ParserInput};
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::{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 input = ParserInput::new(value);
395    let mut parser = Parser::new(&mut input);
396    // FIXME(emilio): why ::empty() instead of ::DEFAULT? Also, what do
397    // browsers do regarding quirks-mode in a media list?
398    let context = parser_context_for_anonymous_content(
399        CssRuleType::Style,
400        ParsingMode::empty(),
401        &ANONYMOUS_CONTENT_URL_DATA,
402    );
403    SourceSizeList::parse(&context, &mut parser)
404}
405
406/// Collect sequence of code points
407/// <https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points>
408pub(crate) fn collect_sequence_characters(
409    s: &str,
410    mut predicate: impl FnMut(&char) -> bool,
411) -> (&str, &str) {
412    let i = s.find(|ch| !predicate(&ch)).unwrap_or(s.len());
413    (&s[0..i], &s[i..])
414}
415
416/// <https://html.spec.whatwg.org/multipage/#valid-non-negative-integer>
417/// TODO(#39315): Use the validation rule from Stylo
418fn is_valid_non_negative_integer_string(s: &str) -> bool {
419    s.chars().all(|c| c.is_ascii_digit())
420}
421
422/// <https://html.spec.whatwg.org/multipage/#valid-floating-point-number>
423/// TODO(#39315): Use the validation rule from Stylo
424fn is_valid_floating_point_number_string(s: &str) -> bool {
425    static RE: LazyLock<Regex> =
426        LazyLock::new(|| Regex::new(r"^-?(?:\d+\.\d+|\d+|\.\d+)(?:(e|E)(\+|\-)?\d+)?$").unwrap());
427
428    RE.is_match(s)
429}
430
431/// Parse an `srcset` attribute:
432/// <https://html.spec.whatwg.org/multipage/#parsing-a-srcset-attribute>.
433pub fn parse_a_srcset_attribute(input: &str) -> Vec<ImageSource> {
434    // > 1. Let input be the value passed to this algorithm.
435    // > 2. Let position be a pointer into input, initially pointing at the start of the string.
436    let mut current_index = 0;
437
438    // > 3. Let candidates be an initially empty source set.
439    let mut candidates = vec![];
440    while current_index < input.len() {
441        let remaining_string = &input[current_index..];
442
443        // > 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or
444        // > U+002C COMMA characters from input given position. If any U+002C COMMA
445        // > characters were collected, that is a parse error.
446        // NOTE: A parse error indicating a non-fatal mismatch between the input and the
447        // requirements will be silently ignored to match the behavior of other browsers.
448        // <https://html.spec.whatwg.org/multipage/#concept-microsyntax-parse-error>
449        let (collected_characters, string_after_whitespace) =
450            collect_sequence_characters(remaining_string, |character| {
451                *character == ',' || character.is_ascii_whitespace()
452            });
453
454        // Add the length of collected whitespace, to find the start of the URL we are going
455        // to parse.
456        current_index += collected_characters.len();
457
458        // > 5. If position is past the end of input, return candidates.
459        if string_after_whitespace.is_empty() {
460            return candidates;
461        }
462
463        // 6. Collect a sequence of code points that are not ASCII whitespace from input
464        // given position, and let that be url.
465        let (url, _) =
466            collect_sequence_characters(string_after_whitespace, |c| !char::is_ascii_whitespace(c));
467
468        // Add the length of `url` that we will parse to advance the index of the next part
469        // of the string to prase.
470        current_index += url.len();
471
472        // 7. Let descriptors be a new empty list.
473        let mut descriptors = Vec::new();
474
475        // > 8. If url ends with U+002C (,), then:
476        // >    1. Remove all trailing U+002C COMMA characters from url. If this removed
477        // >       more than one character, that is a parse error.
478        if url.ends_with(',') {
479            let image_source = ImageSource {
480                url: url.trim_end_matches(',').into(),
481                descriptor: Descriptor {
482                    width: None,
483                    density: None,
484                },
485            };
486            candidates.push(image_source);
487            continue;
488        }
489
490        // Otherwise:
491        // > 8.1. Descriptor tokenizer: Skip ASCII whitespace within input given position.
492        let descriptors_string = &input[current_index..];
493        let (spaces, descriptors_string) =
494            collect_sequence_characters(descriptors_string, |character| {
495                character.is_ascii_whitespace()
496            });
497        current_index += spaces.len();
498
499        // > 8.2. Let current descriptor be the empty string.
500        let mut current_descriptor = String::new();
501
502        // > 8.3. Let state be "in descriptor".
503        let mut state = ParseState::InDescriptor;
504
505        // > 8.4. Let c be the character at position. Do the following depending on the value of
506        // > state. For the purpose of this step, "EOF" is a special character representing
507        // > that position is past the end of input.
508        let mut characters = descriptors_string.chars();
509        let mut character = characters.next();
510        if let Some(character) = character {
511            current_index += character.len_utf8();
512        }
513
514        loop {
515            match (state, character) {
516                (ParseState::InDescriptor, Some(character)) if character.is_ascii_whitespace() => {
517                    // > If current descriptor is not empty, append current descriptor to
518                    // > descriptors and let current descriptor be the empty string. Set
519                    // > state to after descriptor.
520                    if !current_descriptor.is_empty() {
521                        descriptors.push(current_descriptor);
522                        current_descriptor = String::new();
523                        state = ParseState::AfterDescriptor;
524                    }
525                },
526                (ParseState::InDescriptor, Some(',')) => {
527                    // > Advance position to the next character in input. If current descriptor
528                    // > is not empty, append current descriptor to descriptors. Jump to the
529                    // > step labeled descriptor parser.
530                    if !current_descriptor.is_empty() {
531                        descriptors.push(current_descriptor);
532                    }
533                    break;
534                },
535                (ParseState::InDescriptor, Some('(')) => {
536                    // > Append c to current descriptor. Set state to in parens.
537                    current_descriptor.push('(');
538                    state = ParseState::InParens;
539                },
540                (ParseState::InDescriptor, Some(character)) => {
541                    // > Append c to current descriptor.
542                    current_descriptor.push(character);
543                },
544                (ParseState::InDescriptor, None) => {
545                    // > If current descriptor is not empty, append current descriptor to
546                    // > descriptors. Jump to the step labeled descriptor parser.
547                    if !current_descriptor.is_empty() {
548                        descriptors.push(current_descriptor);
549                    }
550                    break;
551                },
552                (ParseState::InParens, Some(')')) => {
553                    // > Append c to current descriptor. Set state to in descriptor.
554                    current_descriptor.push(')');
555                    state = ParseState::InDescriptor;
556                },
557                (ParseState::InParens, Some(character)) => {
558                    // Append c to current descriptor.
559                    current_descriptor.push(character);
560                },
561                (ParseState::InParens, None) => {
562                    // > Append current descriptor to descriptors. Jump to the step
563                    // > labeled descriptor parser.
564                    descriptors.push(current_descriptor);
565                    break;
566                },
567                (ParseState::AfterDescriptor, Some(character))
568                    if character.is_ascii_whitespace() =>
569                {
570                    // > Stay in this state.
571                },
572                (ParseState::AfterDescriptor, Some(_)) => {
573                    // > Set state to in descriptor. Set position to the previous
574                    // > character in input.
575                    state = ParseState::InDescriptor;
576                    continue;
577                },
578                (ParseState::AfterDescriptor, None) => {
579                    // > Jump to the step labeled descriptor parser.
580                    break;
581                },
582            }
583
584            character = characters.next();
585            if let Some(character) = character {
586                current_index += character.len_utf8();
587            }
588        }
589
590        // > 9. Descriptor parser: Let error be no.
591        let mut error = false;
592        // > 10. Let width be absent.
593        let mut width: Option<u32> = None;
594        // > 11. Let density be absent.
595        let mut density: Option<f64> = None;
596        // > 12. Let future-compat-h be absent.
597        let mut future_compat_h: Option<u32> = None;
598
599        // > 13. For each descriptor in descriptors, run the appropriate set of steps from
600        // > the following list:
601        for descriptor in descriptors.into_iter() {
602            let Some(last_character) = descriptor.chars().last() else {
603                break;
604            };
605
606            let first_part_of_string = &descriptor[0..descriptor.len() - last_character.len_utf8()];
607            match last_character {
608                // > If the descriptor consists of a valid non-negative integer followed by a
609                // > U+0077 LATIN SMALL LETTER W character
610                // > 1. If the user agent does not support the sizes attribute, let error be yes.
611                // > 2. If width and density are not both absent, then let error be yes.
612                // > 3. Apply the rules for parsing non-negative integers to the descriptor.
613                // >    If the result is 0, let error be yes. Otherwise, let width be the result.
614                'w' if is_valid_non_negative_integer_string(first_part_of_string) &&
615                    density.is_none() &&
616                    width.is_none() =>
617                {
618                    match parse_unsigned_integer(first_part_of_string.chars()) {
619                        Ok(number) if number > 0 => {
620                            width = Some(number);
621                            continue;
622                        },
623                        _ => error = true,
624                    }
625                },
626
627                // > If the descriptor consists of a valid floating-point number followed by a
628                // > U+0078 LATIN SMALL LETTER X character
629                // > 1. If width, density and future-compat-h are not all absent, then let
630                // >    error be yes.
631                // > 2. Apply the rules for parsing floating-point number values to the
632                // >    descriptor. If the result is less than 0, let error be yes. Otherwise, let
633                // >    density be the result.
634                //
635                // The HTML specification has a procedure for parsing floats that is different enough from
636                // the one that stylo uses, that it's better to use Rust's float parser here. This is
637                // what Gecko does, but it also checks to see if the number is a valid HTML-spec compliant
638                // number first. Not doing that means that we might be parsing numbers that otherwise
639                // wouldn't parse.
640                'x' if is_valid_floating_point_number_string(first_part_of_string) &&
641                    width.is_none() &&
642                    density.is_none() &&
643                    future_compat_h.is_none() =>
644                {
645                    match first_part_of_string.parse::<f64>() {
646                        Ok(number) if number.is_finite() && number >= 0. => {
647                            density = Some(number);
648                            continue;
649                        },
650                        _ => error = true,
651                    }
652                },
653
654                // > If the descriptor consists of a valid non-negative integer followed by a
655                // > U+0068 LATIN SMALL LETTER H character
656                // >   This is a parse error.
657                // > 1. If future-compat-h and density are not both absent, then let error be
658                // >    yes.
659                // > 2. Apply the rules for parsing non-negative integers to the descriptor.
660                // >    If the result is 0, let error be yes. Otherwise, let future-compat-h be the
661                // >    result.
662                'h' if is_valid_non_negative_integer_string(first_part_of_string) &&
663                    future_compat_h.is_none() &&
664                    density.is_none() =>
665                {
666                    match parse_unsigned_integer(first_part_of_string.chars()) {
667                        Ok(number) if number > 0 => {
668                            future_compat_h = Some(number);
669                            continue;
670                        },
671                        _ => error = true,
672                    }
673                },
674
675                // > Anything else
676                // >  Let error be yes.
677                _ => error = true,
678            }
679
680            if error {
681                break;
682            }
683        }
684
685        // > 14. If future-compat-h is not absent and width is absent, let error be yes.
686        if future_compat_h.is_some() && width.is_none() {
687            error = true;
688        }
689
690        // Step 15. If error is still no, then append a new image source to candidates whose URL is
691        // url, associated with a width width if not absent and a pixel density density if not
692        // absent. Otherwise, there is a parse error.
693        if !error {
694            let image_source = ImageSource {
695                url: url.into(),
696                descriptor: Descriptor { width, density },
697            };
698            candidates.push(image_source);
699        }
700
701        // Step 16. Return to the step labeled splitting loop.
702    }
703    candidates
704}
705
706/// Returns true if the given image MIME type is supported.
707fn is_supported_image_mime_type(input: &str) -> bool {
708    // Remove any leading and trailing HTTP whitespace from input.
709    let mime_type = input.trim();
710
711    // <https://mimesniff.spec.whatwg.org/#mime-type-essence>
712    let mime_type_essence = match mime_type.find(';') {
713        Some(semi) => &mime_type[..semi],
714        _ => mime_type,
715    };
716
717    // The HTML specification says the type attribute may be present and if present, the value
718    // must be a valid MIME type string. However an empty type attribute is implicitly supported
719    // to match the behavior of other browsers.
720    // <https://html.spec.whatwg.org/multipage/#attr-source-type>
721    if mime_type_essence.is_empty() {
722        return true;
723    }
724
725    SUPPORTED_IMAGE_MIME_TYPES.contains(&mime_type_essence)
726}