/src/serenity/Userland/Libraries/LibWeb/HTML/SourceSet.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Function.h> |
8 | | #include <AK/HashMap.h> |
9 | | #include <AK/QuickSort.h> |
10 | | #include <LibWeb/Bindings/MainThreadVM.h> |
11 | | #include <LibWeb/CSS/Parser/Parser.h> |
12 | | #include <LibWeb/DOM/Document.h> |
13 | | #include <LibWeb/HTML/SourceSet.h> |
14 | | #include <LibWeb/Infra/CharacterTypes.h> |
15 | | #include <LibWeb/Layout/Node.h> |
16 | | |
17 | | namespace Web::HTML { |
18 | | |
19 | | SourceSet::SourceSet() |
20 | 0 | : m_source_size(CSS::Length::make_auto()) |
21 | 0 | { |
22 | 0 | } |
23 | | |
24 | | bool SourceSet::is_empty() const |
25 | 0 | { |
26 | 0 | return m_sources.is_empty(); |
27 | 0 | } |
28 | | |
29 | | static double pixel_density(ImageSource const& image_source) |
30 | 0 | { |
31 | 0 | return image_source.descriptor.get<ImageSource::PixelDensityDescriptorValue>().value; |
32 | 0 | } |
33 | | |
34 | | // https://html.spec.whatwg.org/multipage/images.html#select-an-image-source-from-a-source-set |
35 | | ImageSourceAndPixelDensity SourceSet::select_an_image_source() |
36 | 0 | { |
37 | | // 1. If an entry b in sourceSet has the same associated pixel density descriptor as an earlier entry a in sourceSet, |
38 | | // then remove entry b. |
39 | | // Repeat this step until none of the entries in sourceSet have the same associated pixel density descriptor |
40 | | // as an earlier entry. |
41 | |
|
42 | 0 | Vector<ImageSource> unique_pixel_density_sources; |
43 | 0 | HashMap<double, ImageSource> unique_pixel_density_sources_map; |
44 | 0 | for (auto const& source : m_sources) { |
45 | 0 | auto source_pixel_density = pixel_density(source); |
46 | 0 | if (!unique_pixel_density_sources_map.contains(source_pixel_density)) { |
47 | 0 | unique_pixel_density_sources.append(source); |
48 | 0 | unique_pixel_density_sources_map.set(source_pixel_density, source); |
49 | 0 | } |
50 | 0 | } |
51 | | |
52 | | // 2. In an implementation-defined manner, choose one image source from sourceSet. Let this be selectedSource. |
53 | | // In our case, select the lowest density greater than 1, otherwise the greatest density available. |
54 | | // 3. Return selectedSource and its associated pixel density. |
55 | |
|
56 | 0 | quick_sort(unique_pixel_density_sources, [](auto& a, auto& b) { |
57 | 0 | return pixel_density(a) < pixel_density(b); |
58 | 0 | }); |
59 | 0 | for (auto const& source : unique_pixel_density_sources) { |
60 | 0 | if (pixel_density(source) >= 1) { |
61 | 0 | return { source, pixel_density(source) }; |
62 | 0 | } |
63 | 0 | } |
64 | | |
65 | 0 | return { unique_pixel_density_sources.last(), pixel_density(unique_pixel_density_sources.last()) }; |
66 | 0 | } |
67 | | |
68 | | static StringView collect_a_sequence_of_code_points(Function<bool(u32 code_point)> condition, StringView input, size_t& position) |
69 | 0 | { |
70 | | // 1. Let result be the empty string. |
71 | | // 2. While position doesn’t point past the end of input and the code point at position within input meets the condition condition: |
72 | | // 1. Append that code point to the end of result. |
73 | | // 2. Advance position by 1. |
74 | | // 3. Return result. |
75 | |
|
76 | 0 | size_t start = position; |
77 | 0 | while (position < input.length() && condition(input[position])) |
78 | 0 | ++position; |
79 | 0 | return input.substring_view(start, position - start); |
80 | 0 | } |
81 | | |
82 | | // https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute |
83 | | SourceSet parse_a_srcset_attribute(StringView input) |
84 | 0 | { |
85 | | // 1. Let input be the value passed to this algorithm. |
86 | | |
87 | | // 2. Let position be a pointer into input, initially pointing at the start of the string. |
88 | 0 | size_t position = 0; |
89 | | |
90 | | // 3. Let candidates be an initially empty source set. |
91 | 0 | SourceSet candidates; |
92 | |
|
93 | 0 | splitting_loop: |
94 | | // 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or U+002C COMMA characters from input given position. |
95 | | // If any U+002C COMMA characters were collected, that is a parse error. |
96 | 0 | collect_a_sequence_of_code_points( |
97 | 0 | [](u32 code_point) { |
98 | 0 | if (code_point == ',') { |
99 | | // FIXME: Report a parse error somehow. |
100 | 0 | return true; |
101 | 0 | } |
102 | 0 | return Infra::is_ascii_whitespace(code_point); |
103 | 0 | }, |
104 | 0 | input, position); |
105 | | |
106 | | // 5. If position is past the end of input, return candidates. |
107 | 0 | if (position >= input.length()) { |
108 | 0 | return candidates; |
109 | 0 | } |
110 | | |
111 | | // 6. Collect a sequence of code points that are not ASCII whitespace from input given position, and let that be url. |
112 | 0 | auto url = collect_a_sequence_of_code_points( |
113 | 0 | [](u32 code_point) { return !Infra::is_ascii_whitespace(code_point); }, |
114 | 0 | input, position); |
115 | | |
116 | | // 7. Let descriptors be a new empty list. |
117 | 0 | Vector<String> descriptors; |
118 | | |
119 | | // 8. If url ends with U+002C (,), then: |
120 | 0 | if (url.ends_with(',')) { |
121 | | // 1. Remove all trailing U+002C COMMA characters from url. If this removed more than one character, that is a parse error. |
122 | 0 | while (url.ends_with(',')) |
123 | 0 | url = url.substring_view(0, url.length() - 1); |
124 | 0 | } |
125 | | // Otherwise: |
126 | 0 | else { |
127 | | // 1. Descriptor tokenizer: Skip ASCII whitespace within input given position. |
128 | 0 | collect_a_sequence_of_code_points( |
129 | 0 | [](u32 code_point) { return Infra::is_ascii_whitespace(code_point); }, |
130 | 0 | input, position); |
131 | | |
132 | | // 2. Let current descriptor be the empty string. |
133 | 0 | StringBuilder current_descriptor; |
134 | |
|
135 | 0 | enum class State { |
136 | 0 | InDescriptor, |
137 | 0 | InParens, |
138 | 0 | AfterDescriptor, |
139 | 0 | }; |
140 | | // 3. Let state be in descriptor. |
141 | 0 | auto state = State::InDescriptor; |
142 | | |
143 | | // 4. Let c be the character at position. Do the following depending on the value of state. |
144 | | // For the purpose of this step, "EOF" is a special character representing that position is past the end of input. |
145 | 0 | for (;;) { |
146 | 0 | Optional<u32> c; |
147 | 0 | if (position < input.length()) { |
148 | 0 | c = input[position]; |
149 | 0 | } |
150 | |
|
151 | 0 | switch (state) { |
152 | | // - In descriptor |
153 | 0 | case State::InDescriptor: |
154 | | // Do the following, depending on the value of c: |
155 | | |
156 | | // - ASCII whitespace |
157 | 0 | if (c.has_value() && Infra::is_ascii_whitespace(c.value())) { |
158 | | // If current descriptor is not empty, append current descriptor to descriptors and let current descriptor be the empty string. |
159 | 0 | if (!current_descriptor.is_empty()) { |
160 | 0 | descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors()); |
161 | 0 | } |
162 | | // Set state to after descriptor. |
163 | 0 | state = State::AfterDescriptor; |
164 | 0 | } |
165 | | // U+002C COMMA (,) |
166 | 0 | else if (c.has_value() && c.value() == ',') { |
167 | | // Advance position to the next character in input. |
168 | 0 | position += 1; |
169 | | |
170 | | // If current descriptor is not empty, append current descriptor to descriptors. |
171 | 0 | if (!current_descriptor.is_empty()) { |
172 | 0 | descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors()); |
173 | 0 | } |
174 | | |
175 | | // Jump to the step labeled descriptor parser. |
176 | 0 | goto descriptor_parser; |
177 | 0 | } |
178 | | |
179 | | // U+0028 LEFT PARENTHESIS (() |
180 | 0 | else if (c.has_value() && c.value() == '(') { |
181 | | // Append c to current descriptor. |
182 | 0 | current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors(); |
183 | | |
184 | | // Set state to in parens. |
185 | 0 | state = State::InParens; |
186 | 0 | } |
187 | | // EOF |
188 | 0 | else if (!c.has_value()) { |
189 | | // If current descriptor is not empty, append current descriptor to descriptors. |
190 | 0 | if (!current_descriptor.is_empty()) { |
191 | 0 | descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors()); |
192 | 0 | } |
193 | | |
194 | | // Jump to the step labeled descriptor parser. |
195 | 0 | goto descriptor_parser; |
196 | 0 | } |
197 | | // Anything else |
198 | 0 | else { |
199 | | // Append c to current descriptor. |
200 | 0 | current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors(); |
201 | 0 | } |
202 | 0 | break; |
203 | | |
204 | | // - In parens |
205 | 0 | case State::InParens: |
206 | | // Do the following, depending on the value of c: |
207 | | // U+0029 RIGHT PARENTHESIS ()) |
208 | 0 | if (c.has_value() && c.value() == ')') { |
209 | | // Append c to current descriptor. |
210 | 0 | current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors(); |
211 | | // Set state to in descriptor. |
212 | 0 | state = State::InDescriptor; |
213 | 0 | } |
214 | | // EOF |
215 | 0 | else if (!c.has_value()) { |
216 | | // Append current descriptor to descriptors. |
217 | 0 | descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors()); |
218 | | |
219 | | // Jump to the step labeled descriptor parser. |
220 | 0 | goto descriptor_parser; |
221 | 0 | } |
222 | | // Anything else |
223 | 0 | else { |
224 | | // Append c to current descriptor. |
225 | 0 | current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors(); |
226 | 0 | } |
227 | 0 | break; |
228 | | |
229 | | // - After descriptor |
230 | 0 | case State::AfterDescriptor: |
231 | | // Do the following, depending on the value of c: |
232 | | // ASCII whitespace |
233 | 0 | if (c.has_value() && Infra::is_ascii_whitespace(c.value())) { |
234 | | // Stay in this state. |
235 | 0 | } |
236 | | // EOF |
237 | 0 | else if (!c.has_value()) { |
238 | | // Jump to the step labeled descriptor parser. |
239 | 0 | goto descriptor_parser; |
240 | 0 | } |
241 | | // Anything else |
242 | 0 | else { |
243 | | // Set state to in descriptor. |
244 | 0 | state = State::InDescriptor; |
245 | | // Set position to the previous character in input. |
246 | 0 | position -= 1; |
247 | 0 | } |
248 | 0 | break; |
249 | 0 | } |
250 | | // Advance position to the next character in input. Repeat this step. |
251 | 0 | position += 1; |
252 | 0 | } |
253 | 0 | } |
254 | 0 | descriptor_parser: |
255 | | // 9. Descriptor parser: Let error be no. |
256 | 0 | bool error = false; |
257 | | |
258 | | // 10. Let width be absent. |
259 | 0 | Optional<int> width; |
260 | | |
261 | | // 11. Let density be absent. |
262 | 0 | Optional<float> density; |
263 | | |
264 | | // 12. Let future-compat-h be absent. |
265 | 0 | Optional<int> future_compat_h; |
266 | | |
267 | | // 13. For each descriptor in descriptors, run the appropriate set of steps from the following list: |
268 | 0 | for (auto& descriptor : descriptors) { |
269 | 0 | auto last_character = descriptor.bytes_as_string_view().bytes().last(); |
270 | 0 | auto descriptor_without_last_character = descriptor.bytes_as_string_view().substring_view(0, descriptor.bytes_as_string_view().length() - 1); |
271 | |
|
272 | 0 | auto as_int = descriptor_without_last_character.to_number<i32>(); |
273 | 0 | auto as_float = descriptor_without_last_character.to_number<float>(); |
274 | | |
275 | | // - If the descriptor consists of a valid non-negative integer followed by a U+0077 LATIN SMALL LETTER W character |
276 | 0 | if (last_character == 'w' && as_int.has_value()) { |
277 | | // NOOP: 1. If the user agent does not support the sizes attribute, let error be yes. |
278 | | |
279 | | // 2. If width and density are not both absent, then let error be yes. |
280 | |
|
281 | 0 | if (width.has_value() || density.has_value()) { |
282 | 0 | error = true; |
283 | 0 | } |
284 | | |
285 | | // FIXME: 3. Apply the rules for parsing non-negative integers to the descriptor. |
286 | | // If the result is zero, let error be yes. Otherwise, let width be the result. |
287 | 0 | width = as_int.value(); |
288 | 0 | } |
289 | | |
290 | | // - If the descriptor consists of a valid floating-point number followed by a U+0078 LATIN SMALL LETTER X character |
291 | 0 | else if (last_character == 'x' && as_float.has_value()) { |
292 | | // 1. If width, density and future-compat-h are not all absent, then let error be yes. |
293 | 0 | if (width.has_value() || density.has_value() || future_compat_h.has_value()) { |
294 | 0 | error = true; |
295 | 0 | } |
296 | | |
297 | | // FIXME: 2. Apply the rules for parsing floating-point number values to the descriptor. |
298 | | // If the result is less than zero, let error be yes. Otherwise, let density be the result. |
299 | 0 | density = as_float.value(); |
300 | 0 | } |
301 | | // - If the descriptor consists of a valid non-negative integer followed by a U+0068 LATIN SMALL LETTER H character |
302 | 0 | else if (last_character == 'h' && as_int.has_value()) { |
303 | | // This is a parse error. |
304 | | // 1. If future-compat-h and density are not both absent, then let error be yes. |
305 | 0 | if (future_compat_h.has_value() || density.has_value()) { |
306 | 0 | error = true; |
307 | 0 | } |
308 | | // FIXME: 2. Apply the rules for parsing non-negative integers to the descriptor. |
309 | | // If the result is zero, let error be yes. Otherwise, let future-compat-h be the result. |
310 | 0 | future_compat_h = as_int.value(); |
311 | 0 | } |
312 | | // - Anything else |
313 | 0 | else { |
314 | | // Let error be yes. |
315 | 0 | error = true; |
316 | 0 | } |
317 | 0 | } |
318 | | |
319 | | // 14. If future-compat-h is not absent and width is absent, let error be yes. |
320 | 0 | if (future_compat_h.has_value() && !width.has_value()) { |
321 | 0 | error = true; |
322 | 0 | } |
323 | | |
324 | | // 15. If error is still no, then append a new image source to candidates whose URL is url, |
325 | | // associated with a width width if not absent and a pixel density density if not absent. |
326 | | // Otherwise, there is a parse error. |
327 | 0 | if (!error) { |
328 | 0 | ImageSource source; |
329 | 0 | source.url = String::from_utf8(url).release_value_but_fixme_should_propagate_errors(); |
330 | 0 | if (width.has_value()) |
331 | 0 | source.descriptor = ImageSource::WidthDescriptorValue { width.value() }; |
332 | 0 | else if (density.has_value()) |
333 | 0 | source.descriptor = ImageSource::PixelDensityDescriptorValue { density.value() }; |
334 | 0 | candidates.m_sources.append(move(source)); |
335 | 0 | } |
336 | | |
337 | | // 16. Return to the step labeled splitting loop. |
338 | 0 | goto splitting_loop; |
339 | 0 | } |
340 | | |
341 | | // https://html.spec.whatwg.org/multipage/images.html#parse-a-sizes-attribute |
342 | | CSS::LengthOrCalculated parse_a_sizes_attribute(DOM::Document const& document, StringView sizes) |
343 | 0 | { |
344 | 0 | auto css_parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext { document }, sizes); |
345 | 0 | return css_parser.parse_as_sizes_attribute(); |
346 | 0 | } |
347 | | |
348 | | // https://html.spec.whatwg.org/multipage/images.html#create-a-source-set |
349 | | SourceSet SourceSet::create(DOM::Element const& element, String default_source, String srcset, String sizes) |
350 | 0 | { |
351 | | // 1. Let source set be an empty source set. |
352 | 0 | SourceSet source_set; |
353 | | |
354 | | // 2. If srcset is not an empty string, then set source set to the result of parsing srcset. |
355 | 0 | if (!srcset.is_empty()) |
356 | 0 | source_set = parse_a_srcset_attribute(srcset); |
357 | | |
358 | | // 3. Let source size be the result of parsing sizes. |
359 | 0 | source_set.m_source_size = parse_a_sizes_attribute(element.document(), sizes); |
360 | | |
361 | | // 4. If default source is not the empty string and source set does not contain an image source |
362 | | // with a pixel density descriptor value of 1, and no image source with a width descriptor, |
363 | | // append default source to source set. |
364 | 0 | if (!default_source.is_empty()) { |
365 | 0 | bool contains_image_source_with_pixel_density_descriptor_value_of_1 = false; |
366 | 0 | bool contains_image_source_with_width_descriptor = false; |
367 | 0 | for (auto& source : source_set.m_sources) { |
368 | 0 | if (source.descriptor.has<ImageSource::PixelDensityDescriptorValue>()) { |
369 | 0 | if (source.descriptor.get<ImageSource::PixelDensityDescriptorValue>().value == 1.0) |
370 | 0 | contains_image_source_with_pixel_density_descriptor_value_of_1 = true; |
371 | 0 | } |
372 | 0 | if (source.descriptor.has<ImageSource::WidthDescriptorValue>()) |
373 | 0 | contains_image_source_with_width_descriptor = true; |
374 | 0 | } |
375 | 0 | if (!contains_image_source_with_pixel_density_descriptor_value_of_1 && !contains_image_source_with_width_descriptor) |
376 | 0 | source_set.m_sources.append({ .url = default_source, .descriptor = {} }); |
377 | 0 | } |
378 | | |
379 | | // 5. Normalize the source densities of source set. |
380 | 0 | source_set.normalize_source_densities(element); |
381 | | |
382 | | // 6. Return source set. |
383 | 0 | return source_set; |
384 | 0 | } |
385 | | |
386 | | // https://html.spec.whatwg.org/multipage/images.html#normalise-the-source-densities |
387 | | void SourceSet::normalize_source_densities(DOM::Element const& element) |
388 | 0 | { |
389 | | // 1. Let source size be source set's source size. |
390 | 0 | auto source_size = [&] { |
391 | 0 | if (!m_source_size.is_calculated()) { |
392 | | // If the source size is viewport-relative, resolve it against the viewport right now. |
393 | 0 | if (m_source_size.value().is_viewport_relative()) { |
394 | 0 | return CSS::Length::make_px(m_source_size.value().viewport_relative_length_to_px(element.document().viewport_rect())); |
395 | 0 | } |
396 | | |
397 | | // FIXME: Resolve font-relative lengths against the relevant font size. |
398 | 0 | return m_source_size.value(); |
399 | 0 | } |
400 | | |
401 | | // HACK: Flush any pending layouts here so we get an up-to-date length resolution context. |
402 | | // FIXME: We should have a way to build a LengthResolutionContext for any DOM node without going through the layout tree. |
403 | 0 | const_cast<DOM::Document&>(element.document()).update_layout(); |
404 | 0 | if (element.layout_node()) { |
405 | 0 | auto context = CSS::Length::ResolutionContext::for_layout_node(*element.layout_node()); |
406 | 0 | return m_source_size.resolved(context); |
407 | 0 | } |
408 | | // FIXME: This is wrong, but we don't have a better way to resolve lengths without a layout node yet. |
409 | 0 | return CSS::Length::make_auto(); |
410 | 0 | }(); |
411 | | |
412 | | // 2. For each image source in source set: |
413 | 0 | for (auto& image_source : m_sources) { |
414 | | // 1. If the image source has a pixel density descriptor, continue to the next image source. |
415 | 0 | if (image_source.descriptor.has<ImageSource::PixelDensityDescriptorValue>()) |
416 | 0 | continue; |
417 | | |
418 | | // 2. Otherwise, if the image source has a width descriptor, |
419 | | // replace the width descriptor with a pixel density descriptor |
420 | | // with a value of the width descriptor value divided by the source size and a unit of x. |
421 | 0 | auto descriptor_value_set = false; |
422 | 0 | if (image_source.descriptor.has<ImageSource::WidthDescriptorValue>()) { |
423 | 0 | auto& width_descriptor = image_source.descriptor.get<ImageSource::WidthDescriptorValue>(); |
424 | 0 | if (source_size.is_absolute()) { |
425 | 0 | auto source_size_in_pixels = source_size.absolute_length_to_px(); |
426 | 0 | if (source_size_in_pixels != 0) { |
427 | 0 | image_source.descriptor = ImageSource::PixelDensityDescriptorValue { |
428 | 0 | .value = (width_descriptor.value / source_size_in_pixels).to_double() |
429 | 0 | }; |
430 | 0 | descriptor_value_set = true; |
431 | 0 | } |
432 | 0 | } else { |
433 | 0 | dbgln("FIXME: Image element has unresolved relative length '{}' in sizes attribute", source_size); |
434 | 0 | } |
435 | 0 | } |
436 | | |
437 | | // 3. Otherwise, give the image source a pixel density descriptor of 1x. |
438 | 0 | if (!descriptor_value_set) { |
439 | 0 | image_source.descriptor = ImageSource::PixelDensityDescriptorValue { |
440 | 0 | .value = 1.0f |
441 | 0 | }; |
442 | 0 | } |
443 | 0 | } |
444 | 0 | } |
445 | | } |