ladybird/Libraries/LibWeb/CSS/CSSFontFaceRule.cpp
Sam Atkins d42a5494d7 LibWeb/CSS: Parse @font-face descriptors as style values
CSSFontFaceRule now stores its values as a CSSFontFaceDescriptors, with
a ParsedFontFace produced on request. This is exposed via the `style`
attribute, so we pass a lot of tests that try to read values from
that.

We have one test regression, which we passed by mistake before: The test
wanted to ensure we don't allow `@font-face` nested inside other rules.
We passed it just because we discarded any `@font-face` without a
`font-family`. What we're supposed to do is 1) keep at-rules with
missing required descriptors and just not use them, and 2) reject
certain ones when nested.

We may want to cache the ParsedFontFace in the future, but I didn't here
because 1) it's called rarely, and 2) that would mean knowing to
invalidate it when the CSSFontFaceDescriptors changes, which isn't
obvious to me right now.
2025-04-03 14:04:21 +01:00

215 lines
8.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2022-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Font/Font.h>
#include <LibGfx/Font/FontStyleMapping.h>
#include <LibWeb/Bindings/CSSFontFaceRulePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::CSS {
GC_DEFINE_ALLOCATOR(CSSFontFaceRule);
GC::Ref<CSSFontFaceRule> CSSFontFaceRule::create(JS::Realm& realm, GC::Ref<CSSFontFaceDescriptors> style)
{
return realm.create<CSSFontFaceRule>(realm, style);
}
CSSFontFaceRule::CSSFontFaceRule(JS::Realm& realm, GC::Ref<CSSFontFaceDescriptors> style)
: CSSRule(realm, Type::FontFace)
, m_style(style)
{
}
void CSSFontFaceRule::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSFontFaceRule);
}
ParsedFontFace CSSFontFaceRule::font_face() const
{
return ParsedFontFace::from_descriptors(m_style);
}
// https://www.w3.org/TR/cssom/#ref-for-cssfontfacerule
String CSSFontFaceRule::serialized() const
{
auto font_face = this->font_face();
StringBuilder builder;
// The result of concatenating the following:
// 1. The string "@font-face {", followed by a single SPACE (U+0020).
builder.append("@font-face { "sv);
// 2. The string "font-family:", followed by a single SPACE (U+0020).
builder.append("font-family: "sv);
// 3. The result of performing serialize a string on the rules font family name.
serialize_a_string(builder, font_face.font_family());
// 4. The string ";", i.e., SEMICOLON (U+003B).
builder.append(';');
// 5. If the rules associated source list is not empty, follow these substeps:
if (!font_face.sources().is_empty()) {
// 1. A single SPACE (U+0020), followed by the string "src:", followed by a single SPACE (U+0020).
builder.append(" src: "sv);
// 2. The result of invoking serialize a comma-separated list on performing serialize a URL or serialize a LOCAL for each source on the source list.
serialize_a_comma_separated_list(builder, font_face.sources(), [&](StringBuilder& builder, ParsedFontFace::Source source) -> void {
source.local_or_url.visit(
[&builder](URL::URL const& url) {
serialize_a_url(builder, url.to_string());
},
[&builder](FlyString const& local) {
// https://drafts.csswg.org/cssom-1/#serialize-a-local
// To serialize a LOCAL means to create a string represented by "local(", followed by the serialization of the LOCAL as a string, followed by ")".
builder.appendff("local({})", serialize_a_string(local));
});
// NOTE: No spec currently exists for format()
if (source.format.has_value()) {
builder.append(" format("sv);
serialize_a_string(builder, source.format.value());
builder.append(")"sv);
}
});
// 3. The string ";", i.e., SEMICOLON (U+003B).
builder.append(';');
}
// 6. If rules associated unicode-range descriptor is present, a single SPACE (U+0020), followed by the string "unicode-range:", followed by a single SPACE (U+0020), followed by the result of performing serialize a <'unicode-range'>, followed by the string ";", i.e., SEMICOLON (U+003B).
builder.append(" unicode-range: "sv);
serialize_unicode_ranges(builder, font_face.unicode_ranges());
builder.append(';');
// FIXME: 7. If rules associated font-variant descriptor is present, a single SPACE (U+0020),
// followed by the string "font-variant:", followed by a single SPACE (U+0020),
// followed by the result of performing serialize a <'font-variant'>,
// followed by the string ";", i.e., SEMICOLON (U+003B).
// 8. If rules associated font-feature-settings descriptor is present, a single SPACE (U+0020),
// followed by the string "font-feature-settings:", followed by a single SPACE (U+0020),
// followed by the result of performing serialize a <'font-feature-settings'>,
// followed by the string ";", i.e., SEMICOLON (U+003B).
if (font_face.font_feature_settings().has_value()) {
auto const& feature_settings = font_face.font_feature_settings().value();
builder.append(" font-feature-settings: "sv);
// NOTE: We sort the tags during parsing, so they're already in the correct order.
bool first = true;
for (auto const& [key, value] : feature_settings) {
if (first) {
first = false;
} else {
builder.append(", "sv);
}
serialize_a_string(builder, key);
// NOTE: 1 is the default value, so don't serialize it.
if (value != 1)
builder.appendff(" {}", value);
}
builder.append(";"sv);
}
// 9. If rules associated font-stretch descriptor is present, a single SPACE (U+0020),
// followed by the string "font-stretch:", followed by a single SPACE (U+0020),
// followed by the result of performing serialize a <'font-stretch'>,
// followed by the string ";", i.e., SEMICOLON (U+003B).
// NOTE: font-stretch is now an alias for font-width, so we use that instead.
if (font_face.width().has_value()) {
builder.append(" font-width: "sv);
// NOTE: font-width is supposed to always be serialized as a percentage.
// Right now, it's stored as a Gfx::FontWidth value, so we have to lossily convert it back.
float percentage = 100.0f;
switch (font_face.width().value()) {
case Gfx::FontWidth::UltraCondensed:
percentage = 50.0f;
break;
case Gfx::FontWidth::ExtraCondensed:
percentage = 62.5f;
break;
case Gfx::FontWidth::Condensed:
percentage = 75.0f;
break;
case Gfx::FontWidth::SemiCondensed:
percentage = 87.5f;
break;
case Gfx::FontWidth::Normal:
percentage = 100.0f;
break;
case Gfx::FontWidth::SemiExpanded:
percentage = 112.5f;
break;
case Gfx::FontWidth::Expanded:
percentage = 125.0f;
break;
case Gfx::FontWidth::ExtraExpanded:
percentage = 150.0f;
break;
case Gfx::FontWidth::UltraExpanded:
percentage = 200.0f;
break;
default:
break;
}
builder.appendff("{}%", percentage);
builder.append(";"sv);
}
// 10. If rules associated font-weight descriptor is present, a single SPACE (U+0020),
// followed by the string "font-weight:", followed by a single SPACE (U+0020),
// followed by the result of performing serialize a <'font-weight'>,
// followed by the string ";", i.e., SEMICOLON (U+003B).
if (font_face.weight().has_value()) {
auto weight = font_face.weight().value();
builder.append(" font-weight: "sv);
if (weight == 400)
builder.append("normal"sv);
else if (weight == 700)
builder.append("bold"sv);
else
builder.appendff("{}", weight);
builder.append(";"sv);
}
// 11. If rules associated font-style descriptor is present, a single SPACE (U+0020),
// followed by the string "font-style:", followed by a single SPACE (U+0020),
// followed by the result of performing serialize a <'font-style'>,
// followed by the string ";", i.e., SEMICOLON (U+003B).
if (font_face.slope().has_value()) {
auto slope = font_face.slope().value();
builder.append(" font-style: "sv);
if (slope == Gfx::name_to_slope("Normal"sv))
builder.append("normal"sv);
else if (slope == Gfx::name_to_slope("Italic"sv))
builder.append("italic"sv);
else {
dbgln("FIXME: CSSFontFaceRule::serialized() does not support slope {}", slope);
builder.append("italic"sv);
}
builder.append(";"sv);
}
// 12. A single SPACE (U+0020), followed by the string "}", i.e., RIGHT CURLY BRACKET (U+007D).
builder.append(" }"sv);
return MUST(builder.to_string());
}
void CSSFontFaceRule::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_style);
}
}