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