1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 22:47:47 +00:00

LibWeb: Implement CSSNumericType

This represents the type of a calculation, which may involve multiplying
or dividing the various numeric types together (eg, length*length, or
length/time, or whatever).

For now, I've made "Return failure" in each algorithm return an empty
Optional. This may or may not be a good solution but we'll see. :^)
This commit is contained in:
Sam Atkins 2023-07-05 20:07:24 +01:00 committed by Andreas Kling
parent 434bac3c67
commit be7093ab0d
3 changed files with 542 additions and 0 deletions

View file

@ -26,6 +26,7 @@ set(SOURCES
CSS/CSSKeyframesRule.cpp
CSS/CSSFontFaceRule.cpp
CSS/CSSMediaRule.cpp
CSS/CSSNumericType.cpp
CSS/CSSRule.cpp
CSS/CSSRuleList.cpp
CSS/CSSStyleDeclaration.cpp

View file

@ -0,0 +1,432 @@
/*
* Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#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>
namespace Web::CSS {
Optional<CSSNumericType::BaseType> CSSNumericType::base_type_from_value_type(ValueType value_type)
{
switch (value_type) {
case ValueType::Angle:
return BaseType::Angle;
case ValueType::Frequency:
return BaseType::Frequency;
case ValueType::Length:
return BaseType::Length;
case ValueType::Percentage:
return BaseType::Percent;
case ValueType::Resolution:
return BaseType::Resolution;
case ValueType::Time:
return BaseType::Time;
case ValueType::Color:
case ValueType::CustomIdent:
case ValueType::FilterValueList:
case ValueType::Image:
case ValueType::Integer:
case ValueType::Paint:
case ValueType::Number:
case ValueType::Position:
case ValueType::Ratio:
case ValueType::Rect:
case ValueType::String:
case ValueType::Url:
return {};
}
VERIFY_NOT_REACHED();
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-create-a-type
Optional<CSSNumericType> CSSNumericType::create_from_unit(StringView unit)
{
// To create a type from a string unit, follow the appropriate branch of the following:
// unit is "number"
if (unit == "number"sv) {
// Return «[ ]» (empty map)
return CSSNumericType {};
}
// unit is "percent"
if (unit == "percent"sv) {
// Return «[ "percent" → 1 ]»
return CSSNumericType { BaseType::Percent, 1 };
}
// unit is a <length> unit
if (Length::unit_from_name(unit).has_value()) {
// Return «[ "length" → 1 ]»
return CSSNumericType { BaseType::Length, 1 };
}
// unit is an <angle> unit
if (Angle::unit_from_name(unit).has_value()) {
// Return «[ "angle" → 1 ]»
return CSSNumericType { BaseType::Angle, 1 };
}
// unit is a <time> unit
if (Time::unit_from_name(unit).has_value()) {
// Return «[ "time" → 1 ]»
return CSSNumericType { BaseType::Time, 1 };
}
// unit is a <frequency> unit
if (Frequency::unit_from_name(unit).has_value()) {
// Return «[ "frequency" → 1 ]»
return CSSNumericType { BaseType::Frequency, 1 };
}
// unit is a <resolution> unit
if (Resolution::unit_from_name(unit).has_value()) {
// Return «[ "resolution" → 1 ]»
return CSSNumericType { BaseType::Resolution, 1 };
}
// unit is a <flex> unit
// FIXME: We don't have <flex> as a type yet.
// Return «[ "flex" → 1 ]»
// anything else
// Return failure.
return {};
// In all cases, the associated percent hint is null.
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-add-two-types
Optional<CSSNumericType> CSSNumericType::added_to(CSSNumericType const& other) const
{
// To add two types type1 and type2, perform the following steps:
// 1. Replace type1 with a fresh copy of type1, and type2 with a fresh copy of type2.
// Let finalType be a new type with an initially empty ordered map and an initially null percent hint.
CSSNumericType type1 = *this;
CSSNumericType type2 = other;
CSSNumericType final_type {};
// 2. If both type1 and type2 have non-null percent hints with different values
if (type1.percent_hint().has_value() && type2.percent_hint().has_value() && type1.percent_hint() != type2.percent_hint()) {
// The types cant be added. Return failure.
return {};
}
// If type1 has a non-null percent hint hint and type2 doesnt
else if (type1.percent_hint().has_value() && !type2.percent_hint().has_value()) {
// Apply the percent hint hint to type2.
type2.apply_percent_hint(type1.percent_hint().value());
}
// Vice versa if type2 has a non-null percent hint and type1 doesnt.
else if (type2.percent_hint().has_value() && !type1.percent_hint().has_value()) {
type1.apply_percent_hint(type2.percent_hint().value());
}
// Otherwise
// Continue to the next step.
// 3. If all the entries of type1 with non-zero values are contained in type2 with the same value, and vice-versa
if (type2.contains_all_the_non_zero_entries_of_other_with_the_same_value(type1)
&& type1.contains_all_the_non_zero_entries_of_other_with_the_same_value(type2)) {
// Copy all of type1s entries to finalType, and then copy all of type2s entries to finalType that
// finalType doesnt already contain. Set finalTypes percent hint to type1s percent hint. Return finalType.
final_type.copy_all_entries_from(type1, SkipIfAlreadyPresent::No);
final_type.copy_all_entries_from(type2, SkipIfAlreadyPresent::Yes);
final_type.set_percent_hint(type1.percent_hint());
return final_type;
}
// If type1 and/or type2 contain "percent" with a non-zero value,
// and type1 and/or type2 contain a key other than "percent" with a non-zero value
else if ((type1.exponent(BaseType::Percent) != 0 || type2.exponent(BaseType::Percent) != 0)
&& (type1.contains_a_key_other_than_percent_with_a_non_zero_value() || type2.contains_a_key_other_than_percent_with_a_non_zero_value())) {
// For each base type other than "percent" hint:
for (auto hint_int = 0; hint_int < to_underlying(BaseType::__Count); ++hint_int) {
auto hint = static_cast<BaseType>(hint_int);
if (hint == BaseType::Percent)
continue;
// 1. Provisionally apply the percent hint hint to both type1 and type2.
auto provisional_type1 = type1;
provisional_type1.apply_percent_hint(hint);
auto provisional_type2 = type2;
provisional_type2.apply_percent_hint(hint);
// 2. If, afterwards, all the entries of type1 with non-zero values are contained in type2
// with the same value, and vice versa, then copy all of type1s entries to finalType,
// and then copy all of type2s entries to finalType that finalType doesnt already contain.
// Set finalTypes percent hint to hint. Return finalType.
if (provisional_type2.contains_all_the_non_zero_entries_of_other_with_the_same_value(provisional_type1)
&& provisional_type1.contains_all_the_non_zero_entries_of_other_with_the_same_value(provisional_type2)) {
final_type.copy_all_entries_from(provisional_type1, SkipIfAlreadyPresent::No);
final_type.copy_all_entries_from(provisional_type2, SkipIfAlreadyPresent::Yes);
final_type.set_percent_hint(hint);
return final_type;
}
// 3. Otherwise, revert type1 and type2 to their state at the start of this loop.
// NOTE: We did the modifications to provisional_type1/2 so this is a no-op.
}
// If the loop finishes without returning finalType, then the types cant be added. Return failure.
return {};
}
// Otherwise
// The types cant be added. Return failure.
return {};
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-multiply-two-types
Optional<CSSNumericType> CSSNumericType::multiplied_by(CSSNumericType const& other) const
{
// To multiply two types type1 and type2, perform the following steps:
// 1. Replace type1 with a fresh copy of type1, and type2 with a fresh copy of type2.
// Let finalType be a new type with an initially empty ordered map and an initially null percent hint.
CSSNumericType type1 = *this;
CSSNumericType type2 = other;
CSSNumericType final_type {};
// 2. If both type1 and type2 have non-null percent hints with different values,
// the types cant be multiplied. Return failure.
if (type1.percent_hint().has_value() && type2.percent_hint().has_value() && type1.percent_hint() != type2.percent_hint())
return {};
// 3. If type1 has a non-null percent hint hint and type2 doesnt, apply the percent hint hint to type2.
if (type1.percent_hint().has_value() && !type2.percent_hint().has_value()) {
type2.apply_percent_hint(type1.percent_hint().value());
}
// Vice versa if type2 has a non-null percent hint and type1 doesnt.
else if (type2.percent_hint().has_value() && !type1.percent_hint().has_value()) {
type1.apply_percent_hint(type2.percent_hint().value());
}
// 4. Copy all of type1s entries to finalType, then for each baseType → power of type2:
final_type.copy_all_entries_from(type1, SkipIfAlreadyPresent::No);
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
if (!type2.exponent(base_type).has_value())
continue;
auto power = type2.exponent(base_type).value();
// 1. If finalType[baseType] exists, increment its value by power.
if (auto exponent = final_type.exponent(base_type); exponent.has_value()) {
final_type.set_exponent(base_type, exponent.value() + power);
}
// 2. Otherwise, set finalType[baseType] to power.
else {
final_type.set_exponent(base_type, power);
}
}
// Set finalTypes percent hint to type1s percent hint.
final_type.set_percent_hint(type1.percent_hint());
// 5. Return finalType.
return final_type;
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-invert-a-type
CSSNumericType CSSNumericType::inverted() const
{
// To invert a type type, perform the following steps:
// 1. Let result be a new type with an initially empty ordered map and an initially null percent hint
CSSNumericType result;
// 2. For each unit → exponent of type, set result[unit] to (-1 * exponent).
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
if (!exponent(base_type).has_value())
continue;
auto power = exponent(base_type).value();
result.set_exponent(base_type, -1 * power);
}
// 3. Return result.
return result;
}
// https://drafts.css-houdini.org/css-typed-om-1/#apply-the-percent-hint
void CSSNumericType::apply_percent_hint(BaseType hint)
{
// To apply the percent hint hint to a type, perform the following steps:
// 1. If type doesnt contain hint, set type[hint] to 0.
if (!exponent(hint).has_value())
set_exponent(hint, 0);
// 2. If type contains "percent", add type["percent"] to type[hint], then set type["percent"] to 0.
if (exponent(BaseType::Percent).has_value()) {
set_exponent(hint, exponent(BaseType::Percent).value() + exponent(hint).value());
set_exponent(BaseType::Percent, 0);
}
// 3. Set types percent hint to hint.
set_percent_hint(hint);
}
bool CSSNumericType::contains_all_the_non_zero_entries_of_other_with_the_same_value(CSSNumericType const& other) const
{
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto other_exponent = other.exponent(static_cast<BaseType>(i));
if (other_exponent.has_value() && other_exponent != 0
&& this->exponent(static_cast<BaseType>(i)) != other_exponent) {
return false;
}
}
return true;
}
bool CSSNumericType::contains_a_key_other_than_percent_with_a_non_zero_value() const
{
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
if (i == to_underlying(BaseType::Percent))
continue;
if (m_type_exponents[i].has_value() && m_type_exponents[i] != 0)
return true;
}
return false;
}
void CSSNumericType::copy_all_entries_from(CSSNumericType const& other, SkipIfAlreadyPresent ignore_existing_values)
{
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
auto exponent = other.exponent(base_type);
if (!exponent.has_value())
continue;
if (ignore_existing_values == SkipIfAlreadyPresent::Yes && this->exponent(base_type).has_value())
continue;
set_exponent(base_type, *exponent);
}
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_dimension(BaseType type) const
{
// A type matches <length> if its only non-zero entry is «[ "length" → 1 ]» and its percent hint is null.
// Similarly for <angle>, <time>, <frequency>, <resolution>, and <flex>.
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);
if (base_type == type) {
if (type_exponent != 1)
return false;
} else {
if (type_exponent.has_value() && type_exponent != 0)
return false;
}
}
return true;
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_percentage() const
{
// A type matches <percentage> if its only non-zero entry is «[ "percent" → 1 ]».
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
auto type_exponent = exponent(base_type);
if (base_type == BaseType::Percent) {
if (!type_exponent.has_value() || type_exponent == 0)
return false;
} else {
if (type_exponent.has_value() && type_exponent != 0)
return false;
}
}
return true;
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_dimension_percentage(BaseType type) const
{
// A type matches <length-percentage> if its only non-zero entry is either «[ "length" → 1 ]»
// or «[ "percent" → 1 ]» Same for <angle-percentage>, <time-percentage>, etc.
// Check for percent -> 1 or type -> 1, but not both
if ((exponent(type) == 1) == (exponent(BaseType::Percent) == 1))
return false;
// Ensure all other types are absent or 0
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
auto type_exponent = exponent(base_type);
if (base_type == type || base_type == BaseType::Percent)
continue;
if (type_exponent.has_value() && type_exponent != 0)
return false;
}
return true;
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_number() const
{
// A type matches <number> if it has no non-zero entries and its percent hint is null.
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);
if (type_exponent.has_value() && type_exponent != 0)
return false;
}
return true;
}
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match
bool CSSNumericType::matches_number_percentage() const
{
// A type matches <number-percentage> if it has no non-zero entries, or its only non-zero entry is «[ "percent" → 1 ]».
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
auto type_exponent = exponent(base_type);
if (base_type == BaseType::Percent && type_exponent.has_value() && type_exponent != 0 && type_exponent != 1) {
return false;
} else if (type_exponent.has_value() && type_exponent != 0) {
return false;
}
}
return true;
}
ErrorOr<String> CSSNumericType::dump() const
{
StringBuilder builder;
TRY(builder.try_appendff("{{ hint: {}", m_percent_hint.map([](auto base_type) { return base_type_name(base_type); })));
for (auto i = 0; i < to_underlying(BaseType::__Count); ++i) {
auto base_type = static_cast<BaseType>(i);
auto type_exponent = exponent(base_type);
if (type_exponent.has_value())
TRY(builder.try_appendff(", \"{}\" → {}", base_type_name(base_type), type_exponent.value()));
}
TRY(builder.try_append(" }"sv));
return builder.to_string();
}
}

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Array.h>
#include <AK/Optional.h>
#include <LibWeb/CSS/PropertyID.h>
namespace Web::CSS {
// https://drafts.css-houdini.org/css-typed-om-1/#numeric-typing
// FIXME: Add IDL for this.
class CSSNumericType {
public:
// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-base-type
enum class BaseType {
Length,
Angle,
Time,
Frequency,
Resolution,
Flex,
Percent,
__Count,
};
static Optional<BaseType> base_type_from_value_type(ValueType);
static constexpr StringView base_type_name(BaseType base_type)
{
switch (base_type) {
case BaseType::Length:
return "length"sv;
case BaseType::Angle:
return "angle"sv;
case BaseType::Time:
return "time"sv;
case BaseType::Frequency:
return "frequency"sv;
case BaseType::Resolution:
return "resolution"sv;
case BaseType::Flex:
return "flex"sv;
case BaseType::Percent:
return "percent"sv;
case BaseType::__Count:
break;
}
VERIFY_NOT_REACHED();
}
static Optional<CSSNumericType> create_from_unit(StringView unit);
CSSNumericType() = default;
CSSNumericType(BaseType type, i32 power)
{
set_exponent(type, power);
}
Optional<CSSNumericType> added_to(CSSNumericType const& other) const;
Optional<CSSNumericType> multiplied_by(CSSNumericType const& other) const;
CSSNumericType inverted() 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_flex_percentage() const { return matches_dimension_percentage(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_number_percentage() const;
bool matches_percentage() const;
bool matches_resolution() const { return matches_dimension(BaseType::Resolution); }
bool matches_resolution_percentage() const { return matches_dimension_percentage(BaseType::Resolution); }
bool matches_time() const { return matches_dimension(BaseType::Time); }
bool matches_time_percentage() const { return matches_dimension_percentage(BaseType::Time); }
Optional<i32> const& exponent(BaseType type) const { return m_type_exponents[to_underlying(type)]; }
void set_exponent(BaseType type, i32 exponent) { m_type_exponents[to_underlying(type)] = exponent; }
Optional<BaseType> const& percent_hint() const { return m_percent_hint; }
void set_percent_hint(Optional<BaseType> hint) { m_percent_hint = hint; }
void apply_percent_hint(BaseType hint);
bool operator==(CSSNumericType const& other) const = default;
ErrorOr<String> dump() const;
private:
bool contains_all_the_non_zero_entries_of_other_with_the_same_value(CSSNumericType const& other) const;
bool contains_a_key_other_than_percent_with_a_non_zero_value() const;
enum class SkipIfAlreadyPresent {
No,
Yes,
};
void copy_all_entries_from(CSSNumericType const& other, SkipIfAlreadyPresent);
bool matches_dimension(BaseType) const;
bool matches_dimension_percentage(BaseType) const;
Array<Optional<i32>, to_underlying(BaseType::__Count)> m_type_exponents;
Optional<BaseType> m_percent_hint;
};
}