Coverage Report

Created: 2026-02-16 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}