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::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}