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}