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}