summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/HTML/SourceSet.cpp
blob: 5584224e4ed0e213e7182012d8d57196c8d5f6ad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/*
 * Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Function.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/HTML/SourceSet.h>
#include <LibWeb/Infra/CharacterTypes.h>

namespace Web::HTML {

SourceSet::SourceSet()
    : m_source_size(CSS::Length::make_auto())
{
}

bool SourceSet::is_empty() const
{
    return m_sources.is_empty();
}

// https://html.spec.whatwg.org/multipage/images.html#select-an-image-source-from-a-source-set
ImageSourceAndPixelDensity SourceSet::select_an_image_source()
{
    // FIXME: 1. If an entry b in sourceSet has the same associated pixel density descriptor as an earlier entry a in sourceSet,
    //           then remove entry b.
    //           Repeat this step until none of the entries in sourceSet have the same associated pixel density descriptor
    //           as an earlier entry.

    // FIXME: 2. In an implementation-defined manner, choose one image source from sourceSet. Let this be selectedSource.

    // FIXME: 3. Return selectedSource and its associated pixel density.

    return { m_sources.first(), 1.0f };
}

static StringView collect_a_sequence_of_code_points(Function<bool(u32 code_point)> condition, StringView input, size_t& position)
{
    // 1. Let result be the empty string.
    // 2. While position doesn’t point past the end of input and the code point at position within input meets the condition condition:
    //    1. Append that code point to the end of result.
    //    2. Advance position by 1.
    // 3. Return result.

    size_t start = position;
    while (position < input.length() && condition(input[position]))
        ++position;
    return input.substring_view(start, position - start);
}

// https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
SourceSet parse_a_srcset_attribute(StringView input)
{
    // 1. Let input be the value passed to this algorithm.

    // 2. Let position be a pointer into input, initially pointing at the start of the string.
    size_t position = 0;

    // 3. Let candidates be an initially empty source set.
    SourceSet candidates;

splitting_loop:
    // 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or U+002C COMMA characters from input given position.
    //    If any U+002C COMMA characters were collected, that is a parse error.
    collect_a_sequence_of_code_points(
        [](u32 code_point) {
            if (code_point == ',') {
                // FIXME: Report a parse error somehow.
                return true;
            }
            return Infra::is_ascii_whitespace(code_point);
        },
        input, position);

    // 5. If position is past the end of input, return candidates.
    if (position >= input.length()) {
        return candidates;
    }

    // 6. Collect a sequence of code points that are not ASCII whitespace from input given position, and let that be url.
    auto url = collect_a_sequence_of_code_points(
        [](u32 code_point) { return !Infra::is_ascii_whitespace(code_point); },
        input, position);

    // 7. Let descriptors be a new empty list.
    Vector<String> descriptors;

    // 8. If url ends with U+002C (,), then:
    if (url.ends_with(',')) {
        // 1. Remove all trailing U+002C COMMA characters from url. If this removed more than one character, that is a parse error.
        while (url.ends_with(','))
            url = url.substring_view(0, url.length() - 1);
    }
    // Otherwise:
    else {
        // 1. Descriptor tokenizer: Skip ASCII whitespace within input given position.
        collect_a_sequence_of_code_points(
            [](u32 code_point) { return Infra::is_ascii_whitespace(code_point); },
            input, position);

        // 2. Let current descriptor be the empty string.
        StringBuilder current_descriptor;

        enum class State {
            InDescriptor,
            InParens,
            AfterDescriptor,
        };
        // 3. Let state be in descriptor.
        auto state = State::InDescriptor;

        // 4. Let c be the character at position. Do the following depending on the value of state.
        //    For the purpose of this step, "EOF" is a special character representing that position is past the end of input.
        for (;;) {
            Optional<u32> c;
            if (position < input.length()) {
                c = input[position];
            }

            switch (state) {
            // - In descriptor
            case State::InDescriptor:
                // Do the following, depending on the value of c:

                // - ASCII whitespace
                if (c.has_value() && Infra::is_ascii_whitespace(c.value())) {
                    // If current descriptor is not empty, append current descriptor to descriptors and let current descriptor be the empty string.
                    if (!current_descriptor.is_empty()) {
                        descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
                    }
                    // Set state to after descriptor.
                    state = State::AfterDescriptor;
                }
                // U+002C COMMA (,)
                else if (c.has_value() && c.value() == ',') {
                    // Advance position to the next character in input.
                    position += 1;

                    // If current descriptor is not empty, append current descriptor to descriptors.
                    if (!current_descriptor.is_empty()) {
                        descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
                    }

                    // Jump to the step labeled descriptor parser.
                    goto descriptor_parser;
                }

                // U+0028 LEFT PARENTHESIS (()
                else if (c.has_value() && c.value() == '(') {
                    // Append c to current descriptor.
                    current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();

                    // Set state to in parens.
                    state = State::InParens;
                }
                // EOF
                else if (!c.has_value()) {
                    // If current descriptor is not empty, append current descriptor to descriptors.
                    if (!current_descriptor.is_empty()) {
                        descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
                    }

                    // Jump to the step labeled descriptor parser.
                    goto descriptor_parser;
                }
                // Anything else
                else {
                    // Append c to current descriptor.
                    current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
                }
                break;

                // - In parens
            case State::InParens:
                // Do the following, depending on the value of c:
                // U+0029 RIGHT PARENTHESIS ())
                if (c.has_value() && c.value() == ')') {
                    // Append c to current descriptor.
                    current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
                    // Set state to in descriptor.
                    state = State::InDescriptor;
                }
                // EOF
                else if (!c.has_value()) {
                    // Append current descriptor to descriptors.
                    descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());

                    // Jump to the step labeled descriptor parser.
                    goto descriptor_parser;
                }
                // Anything else
                else {
                    // Append c to current descriptor.
                    current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
                }
                break;

                // - After descriptor
            case State::AfterDescriptor:
                // Do the following, depending on the value of c:
                // ASCII whitespace
                if (c.has_value() && Infra::is_ascii_whitespace(c.value())) {
                    // Stay in this state.
                }
                // EOF
                else if (!c.has_value()) {
                    // Jump to the step labeled descriptor parser.
                    goto descriptor_parser;
                }
                // Anything else
                else {
                    // Set state to in descriptor.
                    state = State::InDescriptor;
                    // Set position to the previous character in input.
                    position -= 1;
                }
                break;
            }
            // Advance position to the next character in input. Repeat this step.
            position += 1;
        }
    }
descriptor_parser:
    // 9. Descriptor parser: Let error be no.
    bool error = false;

    // 10. Let width be absent.
    Optional<int> width;

    // 11. Let density be absent.
    Optional<float> density;

    // 12. Let future-compat-h be absent.
    Optional<int> future_compat_h;

    // 13. For each descriptor in descriptors, run the appropriate set of steps from the following list:
    for (auto& descriptor : descriptors) {
        auto last_character = descriptor.bytes_as_string_view().bytes().last();
        auto descriptor_without_last_character = descriptor.bytes_as_string_view().substring_view(0, descriptor.bytes_as_string_view().length() - 1);

        auto as_int = descriptor_without_last_character.to_int<i32>();
        auto as_float = descriptor_without_last_character.to_float();

        // - If the descriptor consists of a valid non-negative integer followed by a U+0077 LATIN SMALL LETTER W character
        if (last_character == 'w' && as_int.has_value()) {
            // NOOP: 1. If the user agent does not support the sizes attribute, let error be yes.

            // 2. If width and density are not both absent, then let error be yes.

            if (width.has_value() || density.has_value()) {
                error = true;
            }

            // FIXME: 3. Apply the rules for parsing non-negative integers to the descriptor.
            //           If the result is zero, let error be yes. Otherwise, let width be the result.
            width = as_int.value();
        }

        // - If the descriptor consists of a valid floating-point number followed by a U+0078 LATIN SMALL LETTER X character
        else if (last_character == 'x' && as_float.has_value()) {
            // 1. If width, density and future-compat-h are not all absent, then let error be yes.
            if (width.has_value() || density.has_value() || future_compat_h.has_value()) {
                error = true;
            }

            // FIXME: 2. Apply the rules for parsing floating-point number values to the descriptor.
            //           If the result is less than zero, let error be yes. Otherwise, let density be the result.
            density = as_float.value();
        }
        // - If the descriptor consists of a valid non-negative integer followed by a U+0068 LATIN SMALL LETTER H character
        else if (last_character == 'h' && as_int.has_value()) {
            // This is a parse error.
            // 1. If future-compat-h and density are not both absent, then let error be yes.
            if (future_compat_h.has_value() || density.has_value()) {
                error = true;
            }
            // FIXME: 2. Apply the rules for parsing non-negative integers to the descriptor.
            //           If the result is zero, let error be yes. Otherwise, let future-compat-h be the result.
            future_compat_h = as_int.value();
        }
        // - Anything else
        else {
            // Let error be yes.
            error = true;
        }
    }

    // 14. If future-compat-h is not absent and width is absent, let error be yes.
    if (future_compat_h.has_value() && !width.has_value()) {
        error = true;
    }

    // 15. If error is still no, then append a new image source to candidates whose URL is url,
    //     associated with a width width if not absent and a pixel density density if not absent.
    //     Otherwise, there is a parse error.
    if (!error) {
        ImageSource source;
        source.url = String::from_utf8(url).release_value_but_fixme_should_propagate_errors();
        if (width.has_value())
            source.descriptor = ImageSource::WidthDescriptorValue { width.value() };
        else if (density.has_value())
            source.descriptor = ImageSource::PixelDensityDescriptorValue { density.value() };
        candidates.m_sources.append(move(source));
    }

    // 16. Return to the step labeled splitting loop.
    goto splitting_loop;
}

// https://html.spec.whatwg.org/multipage/images.html#parse-a-sizes-attribute
CSS::Length parse_a_sizes_attribute(DOM::Document const& document, StringView sizes)
{
    auto css_parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext { document }, sizes).release_value_but_fixme_should_propagate_errors();
    return css_parser.parse_as_sizes_attribute();
}

// https://html.spec.whatwg.org/multipage/images.html#create-a-source-set
SourceSet SourceSet::create(DOM::Document const& document, String default_source, String srcset, String sizes)
{
    // 1. Let source set be an empty source set.
    SourceSet source_set;

    // 2. If srcset is not an empty string, then set source set to the result of parsing srcset.
    if (!srcset.is_empty())
        source_set = parse_a_srcset_attribute(srcset);

    // 3. Let source size be the result of parsing sizes.
    auto source_size = parse_a_sizes_attribute(document, sizes);

    // 4. If default source is not the empty string and source set does not contain an image source
    //    with a pixel density descriptor value of 1, and no image source with a width descriptor,
    //    append default source to source set.
    if (!default_source.is_empty()) {
        bool contains_image_source_with_pixel_density_descriptor_value_of_1 = false;
        bool contains_image_source_with_width_descriptor = false;
        for (auto& source : source_set.m_sources) {
            if (source.descriptor.has<ImageSource::PixelDensityDescriptorValue>()) {
                if (source.descriptor.get<ImageSource::PixelDensityDescriptorValue>().value == 1.0f)
                    contains_image_source_with_pixel_density_descriptor_value_of_1 = true;
            }
            if (source.descriptor.has<ImageSource::WidthDescriptorValue>())
                contains_image_source_with_width_descriptor = true;
        }
        if (!contains_image_source_with_pixel_density_descriptor_value_of_1 && !contains_image_source_with_width_descriptor)
            source_set.m_sources.append({ .url = default_source, .descriptor = {} });
    }

    // 5. Normalize the source densities of source set.
    source_set.normalize_source_densities();

    // 6. Return source set.
    return source_set;
}

// https://html.spec.whatwg.org/multipage/images.html#normalise-the-source-densities
void SourceSet::normalize_source_densities()
{
    // 1. Let source size be source set's source size.
    auto source_size = m_source_size;

    // 2. For each image source in source set:
    for (auto& image_source : m_sources) {
        // 1. If the image source has a pixel density descriptor, continue to the next image source.
        if (image_source.descriptor.has<ImageSource::PixelDensityDescriptorValue>())
            continue;

        // 2. Otherwise, if the image source has a width descriptor,
        //    replace the width descriptor with a pixel density descriptor
        //    with a value of the width descriptor value divided by the source size and a unit of x.
        if (image_source.descriptor.has<ImageSource::WidthDescriptorValue>()) {
            auto& width_descriptor = image_source.descriptor.get<ImageSource::WidthDescriptorValue>();
            if (source_size.is_absolute()) {
                image_source.descriptor = ImageSource::PixelDensityDescriptorValue {
                    .value = (width_descriptor.value / source_size.absolute_length_to_px()).value()
                };
            } else {
                dbgln("FIXME: Handle relative sizes: {}", source_size);
                image_source.descriptor = ImageSource::PixelDensityDescriptorValue {
                    .value = 1,
                };
            }
        }

        // 3. Otherwise, give the image source a pixel density descriptor of 1x.
        else {
            image_source.descriptor = ImageSource::PixelDensityDescriptorValue {
                .value = 1.0f
            };
        }
    }
}
}