Serenity Operating System
1/*
2 * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/StringBuilder.h>
8#include <AK/Utf8View.h>
9#include <LibWeb/CSS/Serialize.h>
10
11namespace Web::CSS {
12
13// https://www.w3.org/TR/cssom-1/#escape-a-character
14ErrorOr<void> escape_a_character(StringBuilder& builder, u32 character)
15{
16 TRY(builder.try_append('\\'));
17 TRY(builder.try_append_code_point(character));
18 return {};
19}
20
21// https://www.w3.org/TR/cssom-1/#escape-a-character-as-code-point
22ErrorOr<void> escape_a_character_as_code_point(StringBuilder& builder, u32 character)
23{
24 TRY(builder.try_appendff("\\{:x} ", character));
25 return {};
26}
27
28// https://www.w3.org/TR/cssom-1/#serialize-an-identifier
29ErrorOr<void> serialize_an_identifier(StringBuilder& builder, StringView ident)
30{
31 Utf8View characters { ident };
32 auto first_character = characters.is_empty() ? 0 : *characters.begin();
33
34 // To serialize an identifier means to create a string represented by the concatenation of,
35 // for each character of the identifier:
36 for (auto character : characters) {
37 // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
38 if (character == 0) {
39 TRY(builder.try_append_code_point(0xFFFD));
40 continue;
41 }
42 // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F,
43 // then the character escaped as code point.
44 if ((character >= 0x0001 && character <= 0x001F) || (character == 0x007F)) {
45 TRY(escape_a_character_as_code_point(builder, character));
46 continue;
47 }
48 // If the character is the first character and is in the range [0-9] (U+0030 to U+0039),
49 // then the character escaped as code point.
50 if (builder.is_empty() && character >= '0' && character <= '9') {
51 TRY(escape_a_character_as_code_point(builder, character));
52 continue;
53 }
54 // If the character is the second character and is in the range [0-9] (U+0030 to U+0039)
55 // and the first character is a "-" (U+002D), then the character escaped as code point.
56 if (builder.length() == 1 && first_character == '-' && character >= '0' && character <= '9') {
57 TRY(escape_a_character_as_code_point(builder, character));
58 continue;
59 }
60 // If the character is the first character and is a "-" (U+002D), and there is no second
61 // character, then the escaped character.
62 if (builder.is_empty() && character == '-' && characters.length() == 1) {
63 TRY(escape_a_character(builder, character));
64 continue;
65 }
66 // If the character is not handled by one of the above rules and is greater than or equal to U+0080, is "-" (U+002D) or "_" (U+005F), or is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to U+005A), or \[a-z] (U+0061 to U+007A), then the character itself.
67 if ((character >= 0x0080)
68 || (character == '-') || (character == '_')
69 || (character >= '0' && character <= '9')
70 || (character >= 'A' && character <= 'Z')
71 || (character >= 'a' && character <= 'z')) {
72 TRY(builder.try_append_code_point(character));
73 continue;
74 }
75 // Otherwise, the escaped character.
76 TRY(escape_a_character(builder, character));
77 }
78 return {};
79}
80
81// https://www.w3.org/TR/cssom-1/#serialize-a-string
82ErrorOr<void> serialize_a_string(StringBuilder& builder, StringView string)
83{
84 Utf8View characters { string };
85
86 // To serialize a string means to create a string represented by '"' (U+0022), followed by the result
87 // of applying the rules below to each character of the given string, followed by '"' (U+0022):
88 TRY(builder.try_append('"'));
89
90 for (auto character : characters) {
91 // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
92 if (character == 0) {
93 TRY(builder.try_append_code_point(0xFFFD));
94 continue;
95 }
96 // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F, the character escaped as code point.
97 if ((character >= 0x0001 && character <= 0x001F) || (character == 0x007F)) {
98 TRY(escape_a_character_as_code_point(builder, character));
99 continue;
100 }
101 // If the character is '"' (U+0022) or "\" (U+005C), the escaped character.
102 if (character == 0x0022 || character == 0x005C) {
103 TRY(escape_a_character(builder, character));
104 continue;
105 }
106 // Otherwise, the character itself.
107 TRY(builder.try_append_code_point(character));
108 }
109
110 TRY(builder.try_append('"'));
111 return {};
112}
113
114// https://www.w3.org/TR/cssom-1/#serialize-a-url
115ErrorOr<void> serialize_a_url(StringBuilder& builder, StringView url)
116{
117 // To serialize a URL means to create a string represented by "url(",
118 // followed by the serialization of the URL as a string, followed by ")".
119 TRY(builder.try_append("url("sv));
120 TRY(serialize_a_string(builder, url.to_deprecated_string()));
121 TRY(builder.try_append(')'));
122 return {};
123}
124
125// https://www.w3.org/TR/cssom-1/#serialize-a-local
126ErrorOr<void> serialize_a_local(StringBuilder& builder, StringView path)
127{
128 // To serialize a LOCAL means to create a string represented by "local(",
129 // followed by the serialization of the LOCAL as a string, followed by ")".
130 TRY(builder.try_append("local("sv));
131 TRY(serialize_a_string(builder, path.to_deprecated_string()));
132 TRY(builder.try_append(')'));
133 return {};
134}
135
136// NOTE: No spec currently exists for serializing a <'unicode-range'>.
137ErrorOr<void> serialize_unicode_ranges(StringBuilder& builder, Vector<UnicodeRange> const& unicode_ranges)
138{
139 TRY(serialize_a_comma_separated_list(builder, unicode_ranges, [](auto& builder, UnicodeRange unicode_range) -> ErrorOr<void> {
140 return serialize_a_string(builder, TRY(unicode_range.to_string()));
141 }));
142 return {};
143}
144
145// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
146ErrorOr<void> serialize_a_srgb_value(StringBuilder& builder, Color color)
147{
148 // The serialized form is derived from the computed value and thus, uses either the rgb() or rgba() form
149 // (depending on whether the alpha is exactly 1, or not), with lowercase letters for the function name.
150 // NOTE: Since we use Gfx::Color, having an "alpha of 1" means its value is 255.
151 if (color.alpha() == 255)
152 TRY(builder.try_appendff("rgb({}, {}, {})"sv, color.red(), color.green(), color.blue()));
153 else
154 TRY(builder.try_appendff("rgba({}, {}, {}, {})"sv, color.red(), color.green(), color.blue(), (float)(color.alpha()) / 255.0f));
155 return {};
156}
157
158ErrorOr<String> escape_a_character(u32 character)
159{
160 StringBuilder builder;
161 TRY(escape_a_character(builder, character));
162 return builder.to_string();
163}
164
165ErrorOr<String> escape_a_character_as_code_point(u32 character)
166{
167 StringBuilder builder;
168 TRY(escape_a_character_as_code_point(builder, character));
169 return builder.to_string();
170}
171
172ErrorOr<String> serialize_an_identifier(StringView ident)
173{
174 StringBuilder builder;
175 TRY(serialize_an_identifier(builder, ident));
176 return builder.to_string();
177}
178
179ErrorOr<String> serialize_a_string(StringView string)
180{
181 StringBuilder builder;
182 TRY(serialize_a_string(builder, string));
183 return builder.to_string();
184}
185
186ErrorOr<String> serialize_a_url(StringView url)
187{
188 StringBuilder builder;
189 TRY(serialize_a_url(builder, url));
190 return builder.to_string();
191}
192
193ErrorOr<String> serialize_a_srgb_value(Color color)
194{
195 StringBuilder builder;
196 TRY(serialize_a_srgb_value(builder, color));
197 return builder.to_string();
198}
199
200}