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