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