LibWeb: Implement time-traveling inheritance for CSS font-size
Some checks are pending
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (macos-14, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

When setting `font-family: monospace;` in CSS, we have to interpret
the keyword font sizes (small, medium, large, etc) as slightly smaller
for historical reasons. Normally the medium font size is 16px, but
for monospace it's 13px.

The way this needs to behave is extremely strange:
When encountering `font-family: monospace`, we have to go back and
replay the CSS cascade as if the medium font size had been 13px all
along. Otherwise relative values like 2em/200%/etc could have gotten
lost in the inheritance chain.

We implement this in a fairly naive way by explicitly checking for
`font-family: monospace` (note: it has to be *exactly* like that,
it can't be `font-family: monospace, Courier` or similar.)
When encountered, we simply walk the element ancestors and re-run the
cascade for the font-size property. This is clumsy and inefficient,
but it does work for the common cases.

Other browsers do more elaborate things that we should eventually care
about as well, such as user-configurable font settings, per-language
behavior, etc. For now, this is just something that allows us to handle
more WPT tests where things fall apart due to unexpected font sizes.

To learn more about the wonders of font-size, see this blog post:
https://manishearth.github.io/blog/2017/08/10/font-size-an-unexpectedly-complex-css-property/
This commit is contained in:
Andreas Kling 2025-02-25 11:47:03 +01:00 committed by Andreas Kling
commit b4e47f198a
Notes: github-actions[bot] 2025-02-25 22:56:32 +00:00
14 changed files with 241 additions and 106 deletions

View file

@ -2437,15 +2437,106 @@ GC::Ptr<ComputedProperties> StyleComputer::compute_style_impl(DOM::Element& elem
return computed_properties;
}
static bool is_monospace(CSSStyleValue const& value)
{
if (value.to_keyword() == Keyword::Monospace)
return true;
if (value.is_value_list()) {
auto const& values = value.as_value_list().values();
if (values.size() == 1 && values[0]->to_keyword() == Keyword::Monospace)
return true;
}
return false;
}
// HACK: This function implements time-travelling inheritance for the font-size property
// in situations where the cascade ended up with `font-family: monospace`.
// In such cases, other browsers will magically change the meaning of keyword font sizes
// *even in earlier stages of the cascade!!* to be relative to the default monospace font size (13px)
// instead of the default font size (16px).
// See this blog post for a lot more details about this weirdness:
// https://manishearth.github.io/blog/2017/08/10/font-size-an-unexpectedly-complex-css-property/
RefPtr<CSSStyleValue> StyleComputer::recascade_font_size_if_needed(
DOM::Element& element,
Optional<CSS::Selector::PseudoElement::Type> pseudo_element,
CascadedProperties& cascaded_properties) const
{
// Check for `font-family: monospace`. Note that `font-family: monospace, AnythingElse` does not trigger this path.
// Some CSS frameworks use `font-family: monospace, monospace` to work around this behavior.
auto font_family_value = cascaded_properties.property(CSS::PropertyID::FontFamily);
if (!font_family_value || !is_monospace(*font_family_value))
return nullptr;
// FIXME: This should be configurable.
constexpr CSSPixels default_monospace_font_size_in_px = 13;
static auto monospace_font_family_name = Platform::FontPlugin::the().generic_font_name(Platform::GenericFont::Monospace);
static auto monospace_font = Gfx::FontDatabase::the().get(monospace_font_family_name, default_monospace_font_size_in_px * 0.75f, 400, Gfx::FontWidth::Normal, 0);
// Reconstruct the line of ancestor elements we need to inherit style from, and then do the cascade again
// but only for the font-size property.
Vector<DOM::Element&> ancestors;
if (pseudo_element.has_value())
ancestors.append(element);
for (auto* ancestor = element.parent_element(); ancestor; ancestor = ancestor->parent_element())
ancestors.append(*ancestor);
NonnullRefPtr<CSSStyleValue> new_font_size = CSS::LengthStyleValue::create(CSS::Length::make_px(default_monospace_font_size_in_px));
CSSPixels current_size_in_px = default_monospace_font_size_in_px;
for (auto& ancestor : ancestors.in_reverse()) {
auto& ancestor_cascaded_properties = *ancestor.cascaded_properties({});
auto font_size_value = ancestor_cascaded_properties.property(CSS::PropertyID::FontSize);
if (!font_size_value)
continue;
if (font_size_value->is_initial() || font_size_value->is_unset()) {
current_size_in_px = default_monospace_font_size_in_px;
continue;
}
if (font_size_value->is_inherit()) {
// Do nothing.
continue;
}
if (font_size_value->is_keyword()) {
current_size_in_px = default_monospace_font_size_in_px * absolute_size_mapping(font_size_value->to_keyword());
continue;
}
if (font_size_value->is_percentage()) {
current_size_in_px = CSSPixels::nearest_value_for(font_size_value->as_percentage().percentage().as_fraction() * current_size_in_px);
continue;
}
if (font_size_value->is_calculated()) {
dbgln("FIXME: Support calc() when time-traveling for monospace font-size");
continue;
}
VERIFY(font_size_value->is_length());
current_size_in_px = font_size_value->as_length().length().to_px(viewport_rect(), Length::FontMetrics { current_size_in_px, monospace_font->with_size(current_size_in_px * 0.75f)->pixel_metrics() }, m_root_element_font_metrics);
};
return CSS::LengthStyleValue::create(CSS::Length::make_px(current_size_in_px));
}
GC::Ref<ComputedProperties> StyleComputer::compute_properties(DOM::Element& element, Optional<Selector::PseudoElement::Type> pseudo_element, CascadedProperties& cascaded_properties) const
{
auto computed_style = document().heap().allocate<CSS::ComputedProperties>();
auto new_font_size = recascade_font_size_if_needed(element, pseudo_element, cascaded_properties);
if (new_font_size)
computed_style->set_property(PropertyID::FontSize, *new_font_size, ComputedProperties::Inherited::No, Important::No);
for (auto i = to_underlying(first_longhand_property_id); i <= to_underlying(last_longhand_property_id); ++i) {
auto property_id = static_cast<CSS::PropertyID>(i);
auto value = cascaded_properties.property(property_id);
auto inherited = ComputedProperties::Inherited::No;
// NOTE: We've already handled font-size above.
if (property_id == PropertyID::FontSize && !value && new_font_size)
continue;
if ((!value && is_inherited_property(property_id))
|| (value && value->is_inherit())) {
if (auto inheritance_parent = element_to_inherit_style_from(&element, pseudo_element)) {