LibWeb/CSS: Use CalcSV's context to determine what percentages are

This lets us implement the `matches_number()` and `matches_dimension()`
methods of `CSSNumericType` to spec, instead of being an ad-hoc hack.
This commit is contained in:
Sam Atkins 2025-01-09 17:23:20 +00:00
parent 6f60c258ce
commit 4e1aa96dce
Notes: github-actions[bot] 2025-01-13 11:00:10 +00:00
5 changed files with 98 additions and 79 deletions

View file

@ -5,11 +5,9 @@
*/
#include "CSSNumericType.h"
#include <AK/HashMap.h>
#include <LibWeb/CSS/Angle.h>
#include <LibWeb/CSS/Frequency.h>
#include <LibWeb/CSS/Length.h>
#include <LibWeb/CSS/Percentage.h>
#include <LibWeb/CSS/Resolution.h>
#include <LibWeb/CSS/Time.h>
@ -374,26 +372,47 @@ Optional<CSSNumericType::BaseType> CSSNumericType::entry_with_value_1_while_all_
return result;
}
static bool matches(CSSNumericType::BaseType base_type, ValueType value_type)
{
switch (base_type) {
case CSSNumericType::BaseType::Length:
return value_type == ValueType::Length;
case CSSNumericType::BaseType::Angle:
return value_type == ValueType::Angle;
case CSSNumericType::BaseType::Time:
return value_type == ValueType::Time;
case CSSNumericType::BaseType::Frequency:
return value_type == ValueType::Frequency;
case CSSNumericType::BaseType::Resolution:
return value_type == ValueType::Resolution;
case CSSNumericType::BaseType::Flex:
return value_type == ValueType::Flex;
case CSSNumericType::BaseType::Percent:
return value_type == ValueType::Percentage;
case CSSNumericType::BaseType::__Count:
default:
return false;
}
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_dimension(BaseType type) const
bool CSSNumericType::matches_dimension(BaseType type, Optional<ValueType> percentages_resolve_as) const
{
// A type matches <length> if its only non-zero entry is «[ "length" → 1 ]».
// Similarly for <angle>, <time>, <frequency>, <resolution>, and <flex>.
//
if (entry_with_value_1_while_all_others_are_0() != type)
return false;
// If the context in which the value is used allows <percentage> values, and those percentages are resolved
// against another type, then for the type to be considered matching it must either have a null percent hint,
// or the percent hint must match the other type.
//
if (percentages_resolve_as.has_value())
return !percent_hint().has_value() || matches(*percent_hint(), *percentages_resolve_as);
// If the context does not allow <percentage> values to be mixed with <length>/etc values (or doesnt allow
// <percentage> values at all, such as border-width), then for the type to be considered matching the percent
// hint must be null.
// FIXME: Somehow we need to know what type percentages would be resolved against.
// I'm not at all sure if this check is correct.
if (percent_hint().has_value() && percent_hint() != type)
return false;
return entry_with_value_1_while_all_others_are_0() == type;
return !percent_hint().has_value();
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
@ -408,34 +427,17 @@ bool CSSNumericType::matches_percentage() const
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_dimension_percentage(BaseType type) const
bool CSSNumericType::matches_dimension_percentage(BaseType type, Optional<ValueType> percentages_resolve_as) const
{
// A type matches <length-percentage> if it matches <length> or matches <percentage>.
// Same for <angle-percentage>, <time-percentage>, etc.
return matches_percentage() || matches_dimension(type);
return matches_percentage() || matches_dimension(type, percentages_resolve_as);
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_number() const
bool CSSNumericType::matches_number(Optional<ValueType> percentages_resolve_as) const
{
// A type matches <number> if it has no non-zero entries.
//
// If the context in which the value is used allows <percentage> values, and those percentages are resolved
// against a type other than <number>, then for the type to be considered matching the percent hint must
// either be null or match the other type.
//
// If the context allows <percentage> values, but either doesnt resolve them against another type or resolves
// them against a <number>, then for the type to be considered matching the percent hint must either be null
// or "percent".
//
// If the context does not allow <percentage> values, then for the type to be considered matching the percent
// hint must be null.
// FIXME: Somehow we need to know what type percentages would be resolved against.
// For now, just require no percent hint.
if (percent_hint().has_value())
return false;
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
auto type_exponent = exponent(base_type);
@ -443,7 +445,21 @@ bool CSSNumericType::matches_number() const
return false;
}
return true;
// If the context in which the value is used allows <percentage> values, and those percentages are resolved
// against a type other than <number>, then for the type to be considered matching the percent hint must
// either be null or match the other type.
if (percentages_resolve_as.has_value() && percentages_resolve_as != ValueType::Number)
return !percent_hint().has_value() || matches(*percent_hint(), *percentages_resolve_as);
// If the context allows <percentage> values, but either doesnt resolve them against another type or resolves
// them against a <number>, then for the type to be considered matching the percent hint must either be null
// or "percent".
if (percentages_resolve_as == ValueType::Number)
return !percent_hint().has_value() || percent_hint() == BaseType::Percent;
// If the context does not allow <percentage> values, then for the type to be considered matching the percent
// hint must be null.
return !percent_hint().has_value();
}
bool CSSNumericType::matches_dimension() const

View file

@ -68,18 +68,18 @@ public:
Optional<CSSNumericType> consistent_type(CSSNumericType const& other) const;
Optional<CSSNumericType> made_consistent_with(CSSNumericType const& other) const;
bool matches_angle() const { return matches_dimension(BaseType::Angle); }
bool matches_angle_percentage() const { return matches_dimension_percentage(BaseType::Angle); }
bool matches_flex() const { return matches_dimension(BaseType::Flex); }
bool matches_frequency() const { return matches_dimension(BaseType::Frequency); }
bool matches_frequency_percentage() const { return matches_dimension_percentage(BaseType::Frequency); }
bool matches_length() const { return matches_dimension(BaseType::Length); }
bool matches_length_percentage() const { return matches_dimension_percentage(BaseType::Length); }
bool matches_number() const;
bool matches_angle(Optional<ValueType> percentages_resolve_as) const { return matches_dimension(BaseType::Angle, percentages_resolve_as); }
bool matches_angle_percentage(Optional<ValueType> percentages_resolve_as) const { return matches_dimension_percentage(BaseType::Angle, percentages_resolve_as); }
bool matches_flex(Optional<ValueType> percentages_resolve_as) const { return matches_dimension(BaseType::Flex, percentages_resolve_as); }
bool matches_frequency(Optional<ValueType> percentages_resolve_as) const { return matches_dimension(BaseType::Frequency, percentages_resolve_as); }
bool matches_frequency_percentage(Optional<ValueType> percentages_resolve_as) const { return matches_dimension_percentage(BaseType::Frequency, percentages_resolve_as); }
bool matches_length(Optional<ValueType> percentages_resolve_as) const { return matches_dimension(BaseType::Length, percentages_resolve_as); }
bool matches_length_percentage(Optional<ValueType> percentages_resolve_as) const { return matches_dimension_percentage(BaseType::Length, percentages_resolve_as); }
bool matches_number(Optional<ValueType> percentages_resolve_as) const;
bool matches_percentage() const;
bool matches_resolution() const { return matches_dimension(BaseType::Resolution); }
bool matches_time() const { return matches_dimension(BaseType::Time); }
bool matches_time_percentage() const { return matches_dimension_percentage(BaseType::Time); }
bool matches_resolution(Optional<ValueType> percentages_resolve_as) const { return matches_dimension(BaseType::Resolution, percentages_resolve_as); }
bool matches_time(Optional<ValueType> percentages_resolve_as) const { return matches_dimension(BaseType::Time, percentages_resolve_as); }
bool matches_time_percentage(Optional<ValueType> percentages_resolve_as) const { return matches_dimension_percentage(BaseType::Time, percentages_resolve_as); }
bool matches_dimension() const;
@ -104,8 +104,8 @@ private:
void copy_all_entries_from(CSSNumericType const& other, SkipIfAlreadyPresent);
Optional<BaseType> entry_with_value_1_while_all_others_are_0() const;
bool matches_dimension(BaseType) const;
bool matches_dimension_percentage(BaseType) const;
bool matches_dimension(BaseType, Optional<ValueType> percentages_resolve_as) const;
bool matches_dimension_percentage(BaseType, Optional<ValueType> percentages_resolve_as) const;
Array<Optional<i32>, to_underlying(BaseType::__Count)> m_type_exponents;
Optional<BaseType> m_percent_hint;

View file

@ -1825,7 +1825,7 @@ bool CalculatedStyleValue::equals(CSSStyleValue const& other) const
Optional<Angle> CalculatedStyleValue::resolve_angle() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_angle())
if (result.type().has_value() && result.type()->matches_angle(m_context.percentages_resolve_as))
return Angle::make_degrees(result.value());
return {};
}
@ -1838,7 +1838,7 @@ Optional<Angle> CalculatedStyleValue::resolve_angle(Layout::Node const& layout_n
Optional<Angle> CalculatedStyleValue::resolve_angle(Length::ResolutionContext const& context) const
{
auto result = m_calculation->resolve(context, {});
if (result.type().has_value() && result.type()->matches_angle())
if (result.type().has_value() && result.type()->matches_angle(m_context.percentages_resolve_as))
return Angle::make_degrees(result.value());
return {};
}
@ -1846,7 +1846,7 @@ Optional<Angle> CalculatedStyleValue::resolve_angle(Length::ResolutionContext co
Optional<Angle> CalculatedStyleValue::resolve_angle_percentage(Angle const& percentage_basis) const
{
auto result = m_calculation->resolve({}, percentage_basis);
if (result.type().has_value() && result.type()->matches_angle())
if (result.type().has_value() && result.type()->matches_angle(m_context.percentages_resolve_as))
return Angle::make_degrees(result.value());
return {};
}
@ -1854,7 +1854,7 @@ Optional<Angle> CalculatedStyleValue::resolve_angle_percentage(Angle const& perc
Optional<Flex> CalculatedStyleValue::resolve_flex() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_flex())
if (result.type().has_value() && result.type()->matches_flex(m_context.percentages_resolve_as))
return Flex::make_fr(result.value());
return {};
}
@ -1862,7 +1862,7 @@ Optional<Flex> CalculatedStyleValue::resolve_flex() const
Optional<Frequency> CalculatedStyleValue::resolve_frequency() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_frequency())
if (result.type().has_value() && result.type()->matches_frequency(m_context.percentages_resolve_as))
return Frequency::make_hertz(result.value());
return {};
}
@ -1870,7 +1870,7 @@ Optional<Frequency> CalculatedStyleValue::resolve_frequency() const
Optional<Frequency> CalculatedStyleValue::resolve_frequency_percentage(Frequency const& percentage_basis) const
{
auto result = m_calculation->resolve({}, percentage_basis);
if (result.type().has_value() && result.type()->matches_frequency())
if (result.type().has_value() && result.type()->matches_frequency(m_context.percentages_resolve_as))
return Frequency::make_hertz(result.value());
return {};
}
@ -1878,7 +1878,7 @@ Optional<Frequency> CalculatedStyleValue::resolve_frequency_percentage(Frequency
Optional<Length> CalculatedStyleValue::resolve_length(Length::ResolutionContext const& context) const
{
auto result = m_calculation->resolve(context, {});
if (result.type().has_value() && result.type()->matches_length())
if (result.type().has_value() && result.type()->matches_length(m_context.percentages_resolve_as))
return Length::make_px(CSSPixels { result.value() });
return {};
}
@ -1901,7 +1901,7 @@ Optional<Length> CalculatedStyleValue::resolve_length_percentage(Layout::Node co
Optional<Length> CalculatedStyleValue::resolve_length_percentage(Length::ResolutionContext const& resolution_context, Length const& percentage_basis) const
{
auto result = m_calculation->resolve(resolution_context, percentage_basis);
if (result.type().has_value() && result.type()->matches_length())
if (result.type().has_value() && result.type()->matches_length(m_context.percentages_resolve_as))
return Length::make_px(CSSPixels { result.value() });
return {};
}
@ -1917,7 +1917,7 @@ Optional<Percentage> CalculatedStyleValue::resolve_percentage() const
Optional<Resolution> CalculatedStyleValue::resolve_resolution() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_resolution())
if (result.type().has_value() && result.type()->matches_resolution(m_context.percentages_resolve_as))
return Resolution::make_dots_per_pixel(result.value());
return {};
}
@ -1925,7 +1925,7 @@ Optional<Resolution> CalculatedStyleValue::resolve_resolution() const
Optional<Time> CalculatedStyleValue::resolve_time() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_time())
if (result.type().has_value() && result.type()->matches_time(m_context.percentages_resolve_as))
return Time::make_seconds(result.value());
return {};
}
@ -1933,7 +1933,7 @@ Optional<Time> CalculatedStyleValue::resolve_time() const
Optional<Time> CalculatedStyleValue::resolve_time_percentage(Time const& percentage_basis) const
{
auto result = m_calculation->resolve({}, percentage_basis);
if (result.type().has_value() && result.type()->matches_time())
if (result.type().has_value() && result.type()->matches_time(m_context.percentages_resolve_as))
return Time::make_seconds(result.value());
return {};
}
@ -1941,7 +1941,7 @@ Optional<Time> CalculatedStyleValue::resolve_time_percentage(Time const& percent
Optional<double> CalculatedStyleValue::resolve_number() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_number())
if (result.type().has_value() && result.type()->matches_number(m_context.percentages_resolve_as))
return result.value();
return {};
}
@ -1949,7 +1949,7 @@ Optional<double> CalculatedStyleValue::resolve_number() const
Optional<double> CalculatedStyleValue::resolve_number(Length::ResolutionContext const& context) const
{
auto result = m_calculation->resolve(context, {});
if (result.type().has_value() && result.type()->matches_number())
if (result.type().has_value() && result.type()->matches_number(m_context.percentages_resolve_as))
return result.value();
return {};
}
@ -1962,7 +1962,7 @@ Optional<double> CalculatedStyleValue::resolve_number(Layout::Node const& layout
Optional<i64> CalculatedStyleValue::resolve_integer() const
{
auto result = m_calculation->resolve({}, {});
if (result.type().has_value() && result.type()->matches_number())
if (result.type().has_value() && result.type()->matches_number(m_context.percentages_resolve_as))
return llround(result.value());
return {};
}
@ -1970,7 +1970,7 @@ Optional<i64> CalculatedStyleValue::resolve_integer() const
Optional<i64> CalculatedStyleValue::resolve_integer(Length::ResolutionContext const& context) const
{
auto result = m_calculation->resolve(context, {});
if (result.type().has_value() && result.type()->matches_number())
if (result.type().has_value() && result.type()->matches_number(m_context.percentages_resolve_as))
return llround(result.value());
return {};
}

View file

@ -69,23 +69,23 @@ public:
virtual String to_string(SerializationMode) const override;
virtual bool equals(CSSStyleValue const& other) const override;
bool resolves_to_angle() const { return m_resolved_type.matches_angle(); }
bool resolves_to_angle_percentage() const { return m_resolved_type.matches_angle_percentage(); }
bool resolves_to_angle() const { return m_resolved_type.matches_angle(m_context.percentages_resolve_as); }
bool resolves_to_angle_percentage() const { return m_resolved_type.matches_angle_percentage(m_context.percentages_resolve_as); }
Optional<Angle> resolve_angle() const;
Optional<Angle> resolve_angle(Layout::Node const& layout_node) const;
Optional<Angle> resolve_angle(Length::ResolutionContext const& context) const;
Optional<Angle> resolve_angle_percentage(Angle const& percentage_basis) const;
bool resolves_to_flex() const { return m_resolved_type.matches_flex(); }
bool resolves_to_flex() const { return m_resolved_type.matches_flex(m_context.percentages_resolve_as); }
Optional<Flex> resolve_flex() const;
bool resolves_to_frequency() const { return m_resolved_type.matches_frequency(); }
bool resolves_to_frequency_percentage() const { return m_resolved_type.matches_frequency_percentage(); }
bool resolves_to_frequency() const { return m_resolved_type.matches_frequency(m_context.percentages_resolve_as); }
bool resolves_to_frequency_percentage() const { return m_resolved_type.matches_frequency_percentage(m_context.percentages_resolve_as); }
Optional<Frequency> resolve_frequency() const;
Optional<Frequency> resolve_frequency_percentage(Frequency const& percentage_basis) const;
bool resolves_to_length() const { return m_resolved_type.matches_length(); }
bool resolves_to_length_percentage() const { return m_resolved_type.matches_length_percentage(); }
bool resolves_to_length() const { return m_resolved_type.matches_length(m_context.percentages_resolve_as); }
bool resolves_to_length_percentage() const { return m_resolved_type.matches_length_percentage(m_context.percentages_resolve_as); }
Optional<Length> resolve_length(Length::ResolutionContext const&) const;
Optional<Length> resolve_length(Layout::Node const& layout_node) const;
Optional<Length> resolve_length_percentage(Layout::Node const&, Length const& percentage_basis) const;
@ -95,15 +95,15 @@ public:
bool resolves_to_percentage() const { return m_resolved_type.matches_percentage(); }
Optional<Percentage> resolve_percentage() const;
bool resolves_to_resolution() const { return m_resolved_type.matches_resolution(); }
bool resolves_to_resolution() const { return m_resolved_type.matches_resolution(m_context.percentages_resolve_as); }
Optional<Resolution> resolve_resolution() const;
bool resolves_to_time() const { return m_resolved_type.matches_time(); }
bool resolves_to_time_percentage() const { return m_resolved_type.matches_time_percentage(); }
bool resolves_to_time() const { return m_resolved_type.matches_time(m_context.percentages_resolve_as); }
bool resolves_to_time_percentage() const { return m_resolved_type.matches_time_percentage(m_context.percentages_resolve_as); }
Optional<Time> resolve_time() const;
Optional<Time> resolve_time_percentage(Time const& percentage_basis) const;
bool resolves_to_number() const { return m_resolved_type.matches_number(); }
bool resolves_to_number() const { return m_resolved_type.matches_number(m_context.percentages_resolve_as); }
Optional<double> resolve_number() const;
Optional<double> resolve_number(Length::ResolutionContext const&) const;
Optional<double> resolve_number(Layout::Node const& layout_node) const;
@ -126,6 +126,8 @@ private:
{
}
Optional<ValueType> percentage_resolved_type() const;
CSSNumericType m_resolved_type;
NonnullOwnPtr<CalculationNode> m_calculation;
CalculationContext m_context;

View file

@ -53,23 +53,23 @@ String generate_calculation_type_check(StringView calculation_variable_name, Str
first_type_check = false;
if (allowed_type_name == "<angle>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_angle()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_angle(percentages_resolve_as)"sv);
} else if (allowed_type_name == "<dimension>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_dimension()"sv);
} else if (allowed_type_name == "<flex>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_flex()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_flex(percentages_resolve_as)"sv);
} else if (allowed_type_name == "<frequency>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_frequency()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_frequency(percentages_resolve_as)"sv);
} else if (allowed_type_name == "<length>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_length()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_length(percentages_resolve_as)"sv);
} else if (allowed_type_name == "<number>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_number()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_number(percentages_resolve_as)"sv);
} else if (allowed_type_name == "<percentage>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_percentage()"sv);
} else if (allowed_type_name == "<resolution>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_resolution()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_resolution(percentages_resolve_as)"sv);
} else if (allowed_type_name == "<time>"sv) {
builder.appendff("{}.{}", calculation_variable_name, "matches_time()"sv);
builder.appendff("{}.{}", calculation_variable_name, "matches_time(percentages_resolve_as)"sv);
} else {
dbgln("I don't know what '{}' is!", allowed_type_name);
VERIFY_NOT_REACHED();
@ -120,6 +120,7 @@ OwnPtr<CalculationNode> Parser::parse_math_function(Function const& function, Ca
{
TokenStream stream { function.value };
auto arguments = parse_a_comma_separated_list_of_component_values(stream);
auto const& percentages_resolve_as = context.percentages_resolve_as;
)~~~");
functions_data.for_each_member([&](auto& name, JsonValue const& value) -> void {