/src/serenity/Userland/Libraries/LibWeb/CSS/FontFaceSet.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org> |
3 | | * Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibJS/Heap/Heap.h> |
9 | | #include <LibJS/Runtime/Realm.h> |
10 | | #include <LibJS/Runtime/Set.h> |
11 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
12 | | #include <LibWeb/Bindings/FontFaceSetPrototype.h> |
13 | | #include <LibWeb/Bindings/Intrinsics.h> |
14 | | #include <LibWeb/CSS/FontFace.h> |
15 | | #include <LibWeb/CSS/FontFaceSet.h> |
16 | | #include <LibWeb/CSS/Parser/Parser.h> |
17 | | #include <LibWeb/CSS/StyleValues/ShorthandStyleValue.h> |
18 | | #include <LibWeb/CSS/StyleValues/StringStyleValue.h> |
19 | | #include <LibWeb/CSS/StyleValues/StyleValueList.h> |
20 | | #include <LibWeb/DOM/Document.h> |
21 | | #include <LibWeb/HTML/EventNames.h> |
22 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
23 | | #include <LibWeb/Platform/EventLoopPlugin.h> |
24 | | #include <LibWeb/WebIDL/Promise.h> |
25 | | |
26 | | namespace Web::CSS { |
27 | | |
28 | | JS_DEFINE_ALLOCATOR(FontFaceSet); |
29 | | |
30 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-fontfaceset |
31 | | JS::NonnullGCPtr<FontFaceSet> FontFaceSet::construct_impl(JS::Realm& realm, Vector<JS::Handle<FontFace>> const& initial_faces) |
32 | 0 | { |
33 | 0 | auto ready_promise = WebIDL::create_promise(realm); |
34 | 0 | auto set_entries = JS::Set::create(realm); |
35 | | |
36 | | // The FontFaceSet constructor, when called, must iterate its initialFaces argument and add each value to its set entries. |
37 | 0 | for (auto const& face : initial_faces) |
38 | 0 | set_entries->set_add(face); |
39 | |
|
40 | 0 | if (set_entries->set_size() == 0) |
41 | 0 | WebIDL::resolve_promise(realm, *ready_promise); |
42 | |
|
43 | 0 | return realm.heap().allocate<FontFaceSet>(realm, realm, ready_promise, set_entries); |
44 | 0 | } |
45 | | |
46 | | JS::NonnullGCPtr<FontFaceSet> FontFaceSet::create(JS::Realm& realm) |
47 | 0 | { |
48 | 0 | return construct_impl(realm, {}); |
49 | 0 | } |
50 | | |
51 | | FontFaceSet::FontFaceSet(JS::Realm& realm, JS::NonnullGCPtr<WebIDL::Promise> ready_promise, JS::NonnullGCPtr<JS::Set> set_entries) |
52 | 0 | : DOM::EventTarget(realm) |
53 | 0 | , m_set_entries(set_entries) |
54 | 0 | , m_ready_promise(ready_promise) |
55 | 0 | { |
56 | 0 | bool const is_ready = ready()->state() == JS::Promise::State::Fulfilled; |
57 | 0 | m_status = is_ready ? Bindings::FontFaceSetLoadStatus::Loaded : Bindings::FontFaceSetLoadStatus::Loading; |
58 | 0 | } |
59 | | |
60 | | void FontFaceSet::initialize(JS::Realm& realm) |
61 | 0 | { |
62 | 0 | Base::initialize(realm); |
63 | |
|
64 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(FontFaceSet); |
65 | 0 | } |
66 | | |
67 | | void FontFaceSet::visit_edges(Cell::Visitor& visitor) |
68 | 0 | { |
69 | 0 | Base::visit_edges(visitor); |
70 | 0 | visitor.visit(m_set_entries); |
71 | 0 | visitor.visit(m_ready_promise); |
72 | 0 | visitor.visit(m_loading_fonts); |
73 | 0 | visitor.visit(m_loaded_fonts); |
74 | 0 | visitor.visit(m_failed_fonts); |
75 | 0 | } |
76 | | |
77 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-add |
78 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<FontFaceSet>> |
79 | | FontFaceSet::add(JS::Handle<FontFace> face) |
80 | 0 | { |
81 | | // 1. If font is already in the FontFaceSet’s set entries, skip to the last step of this algorithm immediately. |
82 | 0 | if (m_set_entries->set_has(face)) |
83 | 0 | return JS::NonnullGCPtr<FontFaceSet>(*this); |
84 | | |
85 | | // 2. If font is CSS-connected, throw an InvalidModificationError exception and exit this algorithm immediately. |
86 | 0 | if (face->is_css_connected()) { |
87 | 0 | return WebIDL::InvalidModificationError::create(realm(), "Cannot add a CSS-connected FontFace to a FontFaceSet"_string); |
88 | 0 | } |
89 | | |
90 | | // 3. Add the font argument to the FontFaceSet’s set entries. |
91 | 0 | m_set_entries->set_add(face); |
92 | | |
93 | | // 4. If font’s status attribute is "loading" |
94 | 0 | if (face->status() == Bindings::FontFaceLoadStatus::Loading) { |
95 | | |
96 | | // 1. If the FontFaceSet’s [[LoadingFonts]] list is empty, switch the FontFaceSet to loading. |
97 | 0 | if (m_loading_fonts.is_empty()) { |
98 | 0 | m_status = Bindings::FontFaceSetLoadStatus::Loading; |
99 | 0 | } |
100 | | |
101 | | // 2. Append font to the FontFaceSet’s [[LoadingFonts]] list. |
102 | 0 | m_loading_fonts.append(*face); |
103 | 0 | } |
104 | | |
105 | | // 5. Return the FontFaceSet. |
106 | 0 | return JS::NonnullGCPtr<FontFaceSet>(*this); |
107 | 0 | } |
108 | | |
109 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-delete |
110 | | bool FontFaceSet::delete_(JS::Handle<FontFace> face) |
111 | 0 | { |
112 | | // 1. If font is CSS-connected, return false and exit this algorithm immediately. |
113 | 0 | if (face->is_css_connected()) { |
114 | 0 | return false; |
115 | 0 | } |
116 | | |
117 | | // 2. Let deleted be the result of removing font from the FontFaceSet’s set entries. |
118 | 0 | bool deleted = m_set_entries->set_remove(face); |
119 | | |
120 | | // 3. If font is present in the FontFaceSet’s [[LoadedFonts]], or [[FailedFonts]] lists, remove it. |
121 | 0 | m_loaded_fonts.remove_all_matching([face](auto const& entry) { return entry == face; }); |
122 | 0 | m_failed_fonts.remove_all_matching([face](auto const& entry) { return entry == face; }); |
123 | | |
124 | | // 4. If font is present in the FontFaceSet’s [[LoadingFonts]] list, remove it. If font was the last item in that list (and so the list is now empty), switch the FontFaceSet to loaded. |
125 | 0 | m_loading_fonts.remove_all_matching([face](auto const& entry) { return entry == face; }); |
126 | |
|
127 | 0 | if (m_loading_fonts.is_empty()) { |
128 | 0 | m_status = Bindings::FontFaceSetLoadStatus::Loaded; |
129 | 0 | } |
130 | |
|
131 | 0 | return deleted; |
132 | 0 | } |
133 | | |
134 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-clear |
135 | | void FontFaceSet::clear() |
136 | 0 | { |
137 | | // FIXME: Do the actual spec steps |
138 | 0 | m_set_entries->set_clear(); |
139 | 0 | } |
140 | | |
141 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-onloading |
142 | | void FontFaceSet::set_onloading(WebIDL::CallbackType* event_handler) |
143 | 0 | { |
144 | 0 | set_event_handler_attribute(HTML::EventNames::loading, event_handler); |
145 | 0 | } |
146 | | |
147 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-onloading |
148 | | WebIDL::CallbackType* FontFaceSet::onloading() |
149 | 0 | { |
150 | 0 | return event_handler_attribute(HTML::EventNames::loading); |
151 | 0 | } |
152 | | |
153 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-onloadingdone |
154 | | void FontFaceSet::set_onloadingdone(WebIDL::CallbackType* event_handler) |
155 | 0 | { |
156 | 0 | set_event_handler_attribute(HTML::EventNames::loadingdone, event_handler); |
157 | 0 | } |
158 | | |
159 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-onloadingdone |
160 | | WebIDL::CallbackType* FontFaceSet::onloadingdone() |
161 | 0 | { |
162 | 0 | return event_handler_attribute(HTML::EventNames::loadingdone); |
163 | 0 | } |
164 | | |
165 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-onloadingerror |
166 | | void FontFaceSet::set_onloadingerror(WebIDL::CallbackType* event_handler) |
167 | 0 | { |
168 | 0 | set_event_handler_attribute(HTML::EventNames::loadingerror, event_handler); |
169 | 0 | } |
170 | | |
171 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-onloadingerror |
172 | | WebIDL::CallbackType* FontFaceSet::onloadingerror() |
173 | 0 | { |
174 | 0 | return event_handler_attribute(HTML::EventNames::loadingerror); |
175 | 0 | } |
176 | | |
177 | | // https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces |
178 | | static WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Set>> find_matching_font_faces(JS::Realm& realm, FontFaceSet& font_face_set, String const& font, String const&) |
179 | 0 | { |
180 | | // 1. Parse font using the CSS value syntax of the font property. If a syntax error occurs, return a syntax error. |
181 | 0 | auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), font); |
182 | 0 | auto property = parser.parse_as_css_value(PropertyID::Font); |
183 | 0 | if (!property) |
184 | 0 | return WebIDL::SyntaxError::create(realm, "Unable to parse font"_string); |
185 | | |
186 | | // If the parsed value is a CSS-wide keyword, return a syntax error. |
187 | 0 | if (property->is_css_wide_keyword()) |
188 | 0 | return WebIDL::SyntaxError::create(realm, "Parsed font is a CSS-wide keyword"_string); |
189 | | |
190 | | // FIXME: Absolutize all relative lengths against the initial values of the corresponding properties. (For example, a |
191 | | // relative font weight like bolder is evaluated against the initial value normal.) |
192 | | |
193 | | // FIXME: 2. If text was not explicitly provided, let it be a string containing a single space character (U+0020 SPACE). |
194 | | |
195 | | // 3. Let font family list be the list of font families parsed from font, and font style be the other font style |
196 | | // attributes parsed from font. |
197 | 0 | auto const& font_family_list = property->as_shorthand().longhand(PropertyID::FontFamily)->as_value_list(); |
198 | | |
199 | | // 4. Let available font faces be the available font faces within source. If the allow system fonts flag is specified, |
200 | | // add all system fonts to available font faces. |
201 | 0 | auto available_font_faces = font_face_set.set_entries(); |
202 | | |
203 | | // 5. Let matched font faces initially be an empty list. |
204 | 0 | auto matched_font_faces = JS::Set::create(realm); |
205 | | |
206 | | // 6. For each family in font family list, use the font matching rules to select the font faces from available font |
207 | | // faces that match the font style, and add them to matched font faces. The use of the unicodeRange attribute means |
208 | | // that this may be more than just a single font face. |
209 | 0 | for (auto const& font_family : font_family_list.values()) { |
210 | | // FIXME: The matching below is super basic. We currently just match font family names by their string value. |
211 | 0 | if (!font_family->is_string()) |
212 | 0 | continue; |
213 | | |
214 | 0 | auto const& font_family_name = font_family->as_string().string_value(); |
215 | |
|
216 | 0 | for (auto font_face_value : *available_font_faces) { |
217 | 0 | auto& font_face = verify_cast<FontFace>(font_face_value.key.as_object()); |
218 | 0 | if (font_face.family() != font_family_name) |
219 | 0 | continue; |
220 | | |
221 | 0 | matched_font_faces->set_add(font_face_value.key); |
222 | 0 | } |
223 | 0 | } |
224 | | |
225 | | // FIXME: 7. If matched font faces is empty, set the found faces flag to false. Otherwise, set it to true. |
226 | | // FIXME: 8. For each font face in matched font faces, if its defined unicode-range does not include the codepoint of at |
227 | | // least one character in text, remove it from the list. |
228 | | |
229 | | // 9. Return matched font faces and the found faces flag. |
230 | 0 | return matched_font_faces; |
231 | 0 | } |
232 | | |
233 | | // https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-load |
234 | | JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Promise>> FontFaceSet::load(String const& font, String const& text) |
235 | 0 | { |
236 | 0 | auto& realm = this->realm(); |
237 | | |
238 | | // 1. Let font face set be the FontFaceSet object this method was called on. Let promise be a newly-created promise object. |
239 | 0 | JS::NonnullGCPtr font_face_set = *this; |
240 | 0 | auto promise = WebIDL::create_promise(realm); |
241 | |
|
242 | 0 | Platform::EventLoopPlugin::the().deferred_invoke([&realm, font_face_set, promise, font, text]() mutable { |
243 | | // 3. Find the matching font faces from font face set using the font and text arguments passed to the function, |
244 | | // and let font face list be the return value (ignoring the found faces flag). If a syntax error was returned, |
245 | | // reject promise with a SyntaxError exception and terminate these steps. |
246 | 0 | auto result = find_matching_font_faces(realm, font_face_set, font, text); |
247 | 0 | if (result.is_error()) { |
248 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; |
249 | 0 | WebIDL::reject_promise(realm, promise, Bindings::dom_exception_to_throw_completion(realm.vm(), result.release_error()).release_value().value()); |
250 | 0 | return; |
251 | 0 | } |
252 | | |
253 | 0 | auto matched_font_faces = result.release_value(); |
254 | | |
255 | | // 4. Queue a task to run the following steps synchronously: |
256 | 0 | HTML::queue_a_task(HTML::Task::Source::FontLoading, nullptr, nullptr, JS::create_heap_function(realm.heap(), [&realm, promise, matched_font_faces] { |
257 | 0 | JS::MarkedVector<JS::NonnullGCPtr<WebIDL::Promise>> promises(realm.heap()); |
258 | | |
259 | | // 1. For all of the font faces in the font face list, call their load() method. |
260 | 0 | for (auto font_face_value : *matched_font_faces) { |
261 | 0 | auto& font_face = verify_cast<FontFace>(font_face_value.key.as_object()); |
262 | 0 | font_face.load(); |
263 | |
|
264 | 0 | promises.append(font_face.font_status_promise()); |
265 | 0 | } |
266 | | |
267 | | // 2. Resolve promise with the result of waiting for all of the [[FontStatusPromise]]s of each font face in |
268 | | // the font face list, in order. |
269 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; |
270 | |
|
271 | 0 | WebIDL::wait_for_all( |
272 | 0 | realm, promises, |
273 | 0 | [&realm, promise](auto const&) { |
274 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; |
275 | 0 | WebIDL::resolve_promise(realm, promise); |
276 | 0 | }, |
277 | 0 | [&realm, promise](auto error) { |
278 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; |
279 | 0 | WebIDL::reject_promise(realm, promise, error); |
280 | 0 | }); |
281 | 0 | })); |
282 | 0 | }); |
283 | | |
284 | | // 2. Return promise. Complete the rest of these steps asynchronously. |
285 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
286 | 0 | } |
287 | | |
288 | | // https://drafts.csswg.org/css-font-loading/#font-face-set-ready |
289 | | JS::NonnullGCPtr<JS::Promise> FontFaceSet::ready() const |
290 | 0 | { |
291 | 0 | return verify_cast<JS::Promise>(*m_ready_promise->promise()); |
292 | 0 | } |
293 | | |
294 | | } |