diff --git a/Tests/LibWeb/Text/expected/css/FontFaceSet-load.txt b/Tests/LibWeb/Text/expected/css/FontFaceSet-load.txt new file mode 100644 index 00000000000..922d5994587 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/FontFaceSet-load.txt @@ -0,0 +1,3 @@ +Load invalid font: PASS +Load CSS keyword as font: PASS +Load valid font: PASS diff --git a/Tests/LibWeb/Text/input/css/FontFaceSet-load.html b/Tests/LibWeb/Text/input/css/FontFaceSet-load.html new file mode 100644 index 00000000000..af79e81d9b7 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/FontFaceSet-load.html @@ -0,0 +1,33 @@ + + diff --git a/Userland/Libraries/LibWeb/CSS/FontFace.h b/Userland/Libraries/LibWeb/CSS/FontFace.h index b2fed616cbf..3e577feea22 100644 --- a/Userland/Libraries/LibWeb/CSS/FontFace.h +++ b/Userland/Libraries/LibWeb/CSS/FontFace.h @@ -79,6 +79,8 @@ public: void load_font_source(); + JS::NonnullGCPtr font_status_promise() { return m_font_status_promise; } + private: FontFace(JS::Realm&, JS::NonnullGCPtr font_status_promise, Vector urls, ByteBuffer data, String family, FontFaceDescriptors const& descriptors); diff --git a/Userland/Libraries/LibWeb/CSS/FontFaceSet.cpp b/Userland/Libraries/LibWeb/CSS/FontFaceSet.cpp index 392c457c8aa..4b7d061f5a5 100644 --- a/Userland/Libraries/LibWeb/CSS/FontFaceSet.cpp +++ b/Userland/Libraries/LibWeb/CSS/FontFaceSet.cpp @@ -8,11 +8,19 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include #include +#include +#include #include namespace Web::CSS { @@ -162,12 +170,115 @@ WebIDL::CallbackType* FontFaceSet::onloadingerror() return event_handler_attribute(HTML::EventNames::loadingerror); } -// https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-load -JS::ThrowCompletionOr> FontFaceSet::load(String const&, String const&) +// https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces +static WebIDL::ExceptionOr> find_matching_font_faces(JS::Realm& realm, FontFaceSet& font_face_set, String const& font, String const&) { - // FIXME: Do the steps - auto promise = WebIDL::create_rejected_promise(realm(), WebIDL::NotSupportedError::create(realm(), "FontFaceSet::load is not yet implemented"_string)); - return verify_cast(*promise->promise()); + // 1. Parse font using the CSS value syntax of the font property. If a syntax error occurs, return a syntax error. + auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), font); + auto property = parser.parse_as_css_value(PropertyID::Font); + if (!property) + return WebIDL::SyntaxError::create(realm, "Unable to parse font"_string); + + // If the parsed value is a CSS-wide keyword, return a syntax error. + if (property->is_css_wide_keyword()) + return WebIDL::SyntaxError::create(realm, "Parsed font is a CSS-wide keyword"_string); + + // FIXME: Absolutize all relative lengths against the initial values of the corresponding properties. (For example, a + // relative font weight like bolder is evaluated against the initial value normal.) + + // FIXME: 2. If text was not explicitly provided, let it be a string containing a single space character (U+0020 SPACE). + + // 3. Let font family list be the list of font families parsed from font, and font style be the other font style + // attributes parsed from font. + auto const& font_family_list = property->as_shorthand().longhand(PropertyID::FontFamily)->as_value_list(); + + // 4. Let available font faces be the available font faces within source. If the allow system fonts flag is specified, + // add all system fonts to available font faces. + auto available_font_faces = font_face_set.set_entries(); + + // 5. Let matched font faces initially be an empty list. + auto matched_font_faces = JS::Set::create(realm); + + // 6. For each family in font family list, use the font matching rules to select the font faces from available font + // faces that match the font style, and add them to matched font faces. The use of the unicodeRange attribute means + // that this may be more than just a single font face. + for (auto const& font_family : font_family_list.values()) { + // FIXME: The matching below is super basic. We currently just match font family names by their string value. + if (!font_family->is_string()) + continue; + + auto const& font_family_name = font_family->as_string().string_value(); + + for (auto font_face_value : *available_font_faces) { + auto& font_face = verify_cast(font_face_value.key.as_object()); + if (font_face.family() != font_family_name) + continue; + + matched_font_faces->set_add(font_face_value.key); + } + } + + // FIXME: 7. If matched font faces is empty, set the found faces flag to false. Otherwise, set it to true. + // FIXME: 8. For each font face in matched font faces, if its defined unicode-range does not include the codepoint of at + // least one character in text, remove it from the list. + + // 9. Return matched font faces and the found faces flag. + return matched_font_faces; +} + +// https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-load +JS::ThrowCompletionOr> FontFaceSet::load(String const& font, String const& text) +{ + auto& realm = this->realm(); + + // 1. Let font face set be the FontFaceSet object this method was called on. Let promise be a newly-created promise object. + JS::NonnullGCPtr font_face_set = *this; + auto promise = WebIDL::create_promise(realm); + + Platform::EventLoopPlugin::the().deferred_invoke([&realm, font_face_set, promise, font, text]() mutable { + // 3. Find the matching font faces from font face set using the font and text arguments passed to the function, + // and let font face list be the return value (ignoring the found faces flag). If a syntax error was returned, + // reject promise with a SyntaxError exception and terminate these steps. + auto result = find_matching_font_faces(realm, font_face_set, font, text); + if (result.is_error()) { + HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::reject_promise(realm, promise, Bindings::dom_exception_to_throw_completion(realm.vm(), result.release_error()).release_value().value()); + return; + } + + auto matched_font_faces = result.release_value(); + + // 4. Queue a task to run the following steps synchronously: + HTML::queue_a_task(HTML::Task::Source::FontLoading, nullptr, nullptr, JS::create_heap_function(realm.heap(), [&realm, promise, matched_font_faces] { + JS::MarkedVector> promises(realm.heap()); + + // 1. For all of the font faces in the font face list, call their load() method. + for (auto font_face_value : *matched_font_faces) { + auto& font_face = verify_cast(font_face_value.key.as_object()); + font_face.load(); + + promises.append(font_face.font_status_promise()); + } + + // 2. Resolve promise with the result of waiting for all of the [[FontStatusPromise]]s of each font face in + // the font face list, in order. + HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + WebIDL::wait_for_all( + realm, promises, + [&realm, promise](auto const&) { + HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::resolve_promise(realm, promise); + }, + [&realm, promise](auto error) { + HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::reject_promise(realm, promise, error); + }); + })); + }); + + // 2. Return promise. Complete the rest of these steps asynchronously. + return JS::NonnullGCPtr { verify_cast(*promise->promise()) }; } // https://drafts.csswg.org/css-font-loading/#font-face-set-ready