From d0f80b40b2a662c9cdd697c1f4880cef83cb205d Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 11 Apr 2023 15:48:06 +0100 Subject: [PATCH] LibWeb: Reimplement CalculatedStyleValue as a calculation node tree VALUES-4 defines the internal representation of `calc()` as a tree of calculation nodes. ( https://www.w3.org/TR/css-values-4/#calc-internal ) VALUES-3 lacked any definition here, so we had our own ad-hoc implementation based around the spec grammar. This commit replaces that with CalculationNodes representing each possible node in the tree. There are no intended functional changes, though we do now support nested calc() which previously did not work. For example: `width: calc( 42 * calc(3 + 7) );` I have added an example of this to our test page. A couple of the layout tests that used `calc()` now return values that are 0.5px different from before. There's no visual difference, so I have updated the tests to use the new results. --- Base/res/html/misc/calc.html | 5 + Tests/LibWeb/Layout/expected/grid/borders.txt | 6 +- Tests/LibWeb/Layout/expected/grid/gap.txt | 6 +- .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 358 +++++---- Userland/Libraries/LibWeb/CSS/Parser/Parser.h | 8 +- .../CSS/StyleValues/CalculatedStyleValue.cpp | 719 +++++++++++------- .../CSS/StyleValues/CalculatedStyleValue.h | 221 ++++-- 7 files changed, 828 insertions(+), 495 deletions(-) diff --git a/Base/res/html/misc/calc.html b/Base/res/html/misc/calc.html index 1141afdb3c..030af4519b 100644 --- a/Base/res/html/misc/calc.html +++ b/Base/res/html/misc/calc.html @@ -34,6 +34,11 @@
+

calc(100px + 30% - calc(120px / calc(2*4 + 3 )))

+
+
+
+

calc(50% + 60px)

diff --git a/Tests/LibWeb/Layout/expected/grid/borders.txt b/Tests/LibWeb/Layout/expected/grid/borders.txt index 61d8110ce7..cec9df1629 100644 --- a/Tests/LibWeb/Layout/expected/grid/borders.txt +++ b/Tests/LibWeb/Layout/expected/grid/borders.txt @@ -103,14 +103,14 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline Box at (8,275.34375) content-size 784x90.9375 children: not-inline BlockContainer <(anonymous)> at (8,275.34375) content-size 0x0 children: inline TextNode <#text> - BlockContainer at (445.434356,285.34375) content-size 337.800018x17.46875 children: inline + BlockContainer at (445.934356,285.34375) content-size 337.300018x17.46875 children: inline line 0 width: 6.34375, height: 17.46875, bottom: 17.46875, baseline: 13.53125 - frag 0 from TextNode start: 0, length: 1, rect: [445.434356,285.34375 6.34375x17.46875] + frag 0 from TextNode start: 0, length: 1, rect: [445.934356,285.34375 6.34375x17.46875] "1" TextNode <#text> BlockContainer <(anonymous)> at (8,275.34375) content-size 0x0 children: inline TextNode <#text> - BlockContainer at (18,338.8125) content-size 339.034362x17.46875 children: inline + BlockContainer at (18,338.8125) content-size 338.534362x17.46875 children: inline line 0 width: 8.8125, height: 17.46875, bottom: 17.46875, baseline: 13.53125 frag 0 from TextNode start: 0, length: 1, rect: [18,338.8125 8.8125x17.46875] "2" diff --git a/Tests/LibWeb/Layout/expected/grid/gap.txt b/Tests/LibWeb/Layout/expected/grid/gap.txt index 3bf3f017a4..e199efc197 100644 --- a/Tests/LibWeb/Layout/expected/grid/gap.txt +++ b/Tests/LibWeb/Layout/expected/grid/gap.txt @@ -38,14 +38,14 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline Box at (8,52.9375) content-size 784x50.9375 children: not-inline BlockContainer <(anonymous)> at (8,52.9375) content-size 0x0 children: inline TextNode <#text> - BlockContainer at (435.434356,52.9375) content-size 357.800018x17.46875 children: inline + BlockContainer at (435.934356,52.9375) content-size 357.300018x17.46875 children: inline line 0 width: 6.34375, height: 17.46875, bottom: 17.46875, baseline: 13.53125 - frag 0 from TextNode start: 0, length: 1, rect: [435.434356,52.9375 6.34375x17.46875] + frag 0 from TextNode start: 0, length: 1, rect: [435.934356,52.9375 6.34375x17.46875] "1" TextNode <#text> BlockContainer <(anonymous)> at (8,52.9375) content-size 0x0 children: inline TextNode <#text> - BlockContainer at (8,86.40625) content-size 359.034362x17.46875 children: inline + BlockContainer at (8,86.40625) content-size 358.534362x17.46875 children: inline line 0 width: 8.8125, height: 17.46875, bottom: 17.46875, baseline: 13.53125 frag 0 from TextNode start: 0, length: 1, rect: [8,86.40625 8.8125x17.46875] "2" diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 49ff0fc7be..5aa739a1eb 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -3271,11 +3271,21 @@ RefPtr Parser::parse_builtin_value(ComponentValue const& component_v RefPtr Parser::parse_calculated_value(Vector const& component_values) { - auto calc_expression = parse_calc_expression(component_values); - if (calc_expression == nullptr) - return nullptr; + auto calculation_tree = parse_a_calculation(component_values).release_value_but_fixme_should_propagate_errors(); - auto calc_type = calc_expression->resolved_type(); + if (calculation_tree == nullptr) { + dbgln_if(CSS_PARSER_DEBUG, "Failed to parse calculation tree"); + return nullptr; + } else { + if constexpr (CSS_PARSER_DEBUG) { + dbgln("Parsed calculation tree:"); + StringBuilder builder; + calculation_tree->dump(builder, 0).release_value_but_fixme_should_propagate_errors(); + dbgln(builder.string_view()); + } + } + + auto calc_type = calculation_tree->resolved_type(); if (!calc_type.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "calc() resolved as invalid!!!"); return nullptr; @@ -3302,7 +3312,7 @@ RefPtr Parser::parse_calculated_value(Vector Parser::parse_dynamic_value(ComponentValue const& component_value) @@ -7085,147 +7095,231 @@ Optional Parser::parse_a_n_plus_b_patt return syntax_error(); } -OwnPtr Parser::parse_calc_expression(Vector const& values) -{ - auto tokens = TokenStream(values); - return parse_calc_sum(tokens); -} +class UnparsedCalculationNode final : public CalculationNode { +public: + static ErrorOr> create(ComponentValue component_value) + { + return adopt_nonnull_own_or_enomem(new (nothrow) UnparsedCalculationNode(move(component_value))); + } + virtual ~UnparsedCalculationNode() = default; -Optional Parser::parse_calc_value(TokenStream& tokens) -{ - auto current_token = tokens.next_token(); + ComponentValue& component_value() { return m_component_value; } - if (current_token.is_block() && current_token.block().is_paren()) { - auto block_values = TokenStream(current_token.block().values()); - auto parsed_calc_sum = parse_calc_sum(block_values); - if (!parsed_calc_sum) - return {}; - return CalculatedStyleValue::CalcValue { parsed_calc_sum.release_nonnull() }; + virtual ErrorOr to_string() const override { VERIFY_NOT_REACHED(); } + virtual Optional resolved_type() const override { VERIFY_NOT_REACHED(); } + virtual bool contains_percentage() const override { VERIFY_NOT_REACHED(); } + virtual CalculatedStyleValue::CalculationResult resolve(Layout::Node const*, CalculatedStyleValue::PercentageBasis const&) const override { VERIFY_NOT_REACHED(); } + + virtual ErrorOr dump(StringBuilder& builder, int indent) const override + { + return builder.try_appendff("{: >{}}UNPARSED({})\n", "", indent, TRY(m_component_value.to_debug_string())); } - if (current_token.is(Token::Type::Number)) - return CalculatedStyleValue::CalcValue { current_token.token().number() }; - - if (current_token.is(Token::Type::Dimension) || current_token.is(Token::Type::Percentage)) { - auto maybe_dimension = parse_dimension(current_token); - if (!maybe_dimension.has_value()) - return {}; - auto& dimension = maybe_dimension.value(); - - if (dimension.is_angle()) - return CalculatedStyleValue::CalcValue { dimension.angle() }; - if (dimension.is_frequency()) - return CalculatedStyleValue::CalcValue { dimension.frequency() }; - if (dimension.is_length()) - return CalculatedStyleValue::CalcValue { dimension.length() }; - if (dimension.is_percentage()) - return CalculatedStyleValue::CalcValue { dimension.percentage() }; - if (dimension.is_resolution()) { - // Resolution is not allowed in calc() - return {}; - } - if (dimension.is_time()) - return CalculatedStyleValue::CalcValue { dimension.time() }; - VERIFY_NOT_REACHED(); +private: + UnparsedCalculationNode(ComponentValue component_value) + : CalculationNode(Type::Unparsed) + , m_component_value(move(component_value)) + { } - return {}; -} - -OwnPtr Parser::parse_calc_product_part_with_operator(TokenStream& tokens) -{ - tokens.skip_whitespace(); - - auto const& op_token = tokens.peek_token(); - if (!op_token.is(Token::Type::Delim)) - return nullptr; - - auto op = op_token.token().delim(); - if (op != '*' && op != '/') - return nullptr; - - tokens.next_token(); - tokens.skip_whitespace(); - auto parsed_calc_value = parse_calc_value(tokens); - if (!parsed_calc_value.has_value()) - return nullptr; - - auto operation = op == '*' - ? CalculatedStyleValue::ProductOperation::Multiply - : CalculatedStyleValue::ProductOperation::Divide; - return make(operation, parsed_calc_value.release_value()); -} - -// https://www.w3.org/TR/css-values-4/#typedef-calc-product -OwnPtr Parser::parse_calc_product(TokenStream& tokens) -{ - // ` = [ [ '*' | '/' ] ]*` - - auto first_calc_value_or_error = parse_calc_value(tokens); - if (!first_calc_value_or_error.has_value()) - return nullptr; - - auto calc_product = make( - first_calc_value_or_error.release_value(), - Vector> {}); - - while (tokens.has_next_token()) { - auto product_with_operator = parse_calc_product_part_with_operator(tokens); - if (!product_with_operator) - break; - calc_product->zero_or_more_additional_calc_values.append(product_with_operator.release_nonnull()); - } - - return calc_product; -} - -OwnPtr Parser::parse_calc_sum_part_with_operator(TokenStream& tokens) -{ - // The following has to have the shape of <+ or -> - // But the first whitespace gets eaten in parse_calc_product_part_with_operator(). - if (!(tokens.peek_token().is(Token::Type::Delim) - && (tokens.peek_token().token().delim() == '+' || tokens.peek_token().token().delim() == '-') - && tokens.peek_token(1).is(Token::Type::Whitespace))) - return nullptr; - - auto const& token = tokens.next_token(); - tokens.skip_whitespace(); - - CalculatedStyleValue::SumOperation op; - auto delim = token.token().delim(); - if (delim == '+') - op = CalculatedStyleValue::SumOperation::Add; - else if (delim == '-') - op = CalculatedStyleValue::SumOperation::Subtract; - else - return nullptr; - - auto calc_product = parse_calc_product(tokens); - if (!calc_product) - return nullptr; - return make(op, calc_product.release_nonnull()); + ComponentValue m_component_value; }; -// https://www.w3.org/TR/css-values-4/#typedef-calc-sum -OwnPtr Parser::parse_calc_sum(TokenStream& tokens) +// https://www.w3.org/TR/css-values-4/#parse-a-calculation +ErrorOr> Parser::parse_a_calculation(Vector const& original_values) { - // ` = [ [ '+' | '-' ] ]*` + // 1. Discard any s from values. + // 2. An item in values is an “operator” if it’s a with the value "+", "-", "*", or "/". Otherwise, it’s a “value”. + struct Operator { + char delim; + }; + using Value = Variant, Operator>; + Vector values; + for (auto& value : original_values) { + if (value.is(Token::Type::Whitespace)) + continue; + if (value.is(Token::Type::Delim)) { + if (first_is_one_of(value.token().delim(), static_cast('+'), static_cast('-'), static_cast('*'), static_cast('/'))) { + // NOTE: Sequential operators are invalid syntax. + if (!values.is_empty() && values.last().has()) + return nullptr; - auto parsed_calc_product = parse_calc_product(tokens); - if (!parsed_calc_product) - return nullptr; + TRY(values.try_append(Operator { static_cast(value.token().delim()) })); + continue; + } + } - Vector> additional {}; - while (tokens.has_next_token()) { - auto calc_sum_part = parse_calc_sum_part_with_operator(tokens); - if (!calc_sum_part) - return nullptr; - additional.append(calc_sum_part.release_nonnull()); + if (value.is(Token::Type::Number)) { + TRY(values.try_append({ TRY(NumericCalculationNode::create(value.token().number())) })); + continue; + } + + if (auto dimension = parse_dimension(value); dimension.has_value()) { + if (dimension->is_angle()) + TRY(values.try_append({ TRY(NumericCalculationNode::create(dimension->angle())) })); + else if (dimension->is_frequency()) + TRY(values.try_append({ TRY(NumericCalculationNode::create(dimension->frequency())) })); + else if (dimension->is_length()) + TRY(values.try_append({ TRY(NumericCalculationNode::create(dimension->length())) })); + else if (dimension->is_percentage()) + TRY(values.try_append({ TRY(NumericCalculationNode::create(dimension->percentage())) })); + // FIXME: Resolutions, once calc() supports them. + else if (dimension->is_time()) + TRY(values.try_append({ TRY(NumericCalculationNode::create(dimension->time())) })); + else + VERIFY_NOT_REACHED(); + continue; + } + + TRY(values.try_append({ TRY(UnparsedCalculationNode::create(value)) })); } - tokens.skip_whitespace(); + // If we have no values, the syntax is invalid. + if (values.is_empty()) + return nullptr; - return make(parsed_calc_product.release_nonnull(), move(additional)); + // NOTE: If the first or last value is an operator, the syntax is invalid. + if (values.first().has() || values.last().has()) + return nullptr; + + // 3. Collect children into Product and Invert nodes. + // For every consecutive run of value items in values separated by "*" or "/" operators: + while (true) { + Optional first_product_operator = values.find_first_index_if([](auto const& item) { + return item.template has() + && first_is_one_of(item.template get().delim, '*', '/'); + }); + + if (!first_product_operator.has_value()) + break; + + auto start_of_run = first_product_operator.value() - 1; + auto end_of_run = first_product_operator.value() + 1; + for (auto i = start_of_run + 1; i < values.size(); i += 2) { + auto& item = values[i]; + if (!item.has()) { + end_of_run = i - 1; + break; + } + + auto delim = item.get().delim; + if (!first_is_one_of(delim, '*', '/')) { + end_of_run = i - 1; + break; + } + } + + // 1. For each "/" operator in the run, replace its right-hand value item rhs with an Invert node containing rhs as its child. + Vector> run_values; + TRY(run_values.try_append(move(values[start_of_run].get>()))); + for (auto i = start_of_run + 1; i <= end_of_run; i += 2) { + auto& operator_ = values[i].get().delim; + auto& rhs = values[i + 1]; + if (operator_ == '/') { + TRY(run_values.try_append(TRY(InvertCalculationNode::create(move(rhs.get>()))))); + continue; + } + VERIFY(operator_ == '*'); + TRY(run_values.try_append(move(rhs.get>()))); + } + // 2. Replace the entire run with a Product node containing the value items of the run as its children. + auto product_node = TRY(ProductCalculationNode::create(move(run_values))); + values.remove(start_of_run, end_of_run - start_of_run + 1); + TRY(values.try_insert(start_of_run, { move(product_node) })); + } + + // 4. Collect children into Sum and Negate nodes. + Optional> single_value; + { + // 1. For each "-" operator item in values, replace its right-hand value item rhs with a Negate node containing rhs as its child. + for (auto i = 0u; i < values.size(); ++i) { + auto& maybe_minus_operator = values[i]; + if (!maybe_minus_operator.has() || maybe_minus_operator.get().delim != '-') + continue; + + auto rhs_index = ++i; + auto& rhs = values[rhs_index]; + + NonnullOwnPtr negate_node = TRY(NegateCalculationNode::create(move(rhs.get>()))); + values.remove(rhs_index); + values.insert(rhs_index, move(negate_node)); + } + + // 2. If values has only one item, and it is a Product node or a parenthesized simple block, replace values with that item. + if (values.size() == 1) { + TRY(values.first().visit( + [&](ComponentValue& component_value) -> ErrorOr { + if (component_value.is_block() && component_value.block().is_paren()) + single_value = TRY(UnparsedCalculationNode::create(component_value)); + return {}; + }, + [&](NonnullOwnPtr& node) -> ErrorOr { + if (node->type() == CalculationNode::Type::Product) + single_value = move(node); + return {}; + }, + [](auto&) -> ErrorOr { return {}; })); + } + // Otherwise, replace values with a Sum node containing the value items of values as its children. + if (!single_value.has_value()) { + values.remove_all_matching([](Value& value) { return value.has(); }); + Vector> value_items; + TRY(value_items.try_ensure_capacity(values.size())); + for (auto& value : values) { + if (value.has()) + continue; + value_items.unchecked_append(move(value.get>())); + } + single_value = TRY(SumCalculationNode::create(move(value_items))); + } + } + + // 5. At this point values is a tree of Sum, Product, Negate, and Invert nodes, with other types of values at the leaf nodes. Process the leaf nodes. + // For every leaf node leaf in values: + bool parsing_failed_for_child_node = false; + TRY(single_value.value()->for_each_child_node([&](NonnullOwnPtr& node) -> ErrorOr { + if (node->type() != CalculationNode::Type::Unparsed) + return {}; + + auto& unparsed_node = static_cast(*node); + auto& component_value = unparsed_node.component_value(); + + // 1. If leaf is a parenthesized simple block, replace leaf with the result of parsing a calculation from leaf’s contents. + if (component_value.is_block() && component_value.block().is_paren()) { + auto leaf_calculation = TRY(parse_a_calculation(component_value.block().values())); + if (!leaf_calculation) { + parsing_failed_for_child_node = true; + return {}; + } + node = leaf_calculation.release_nonnull(); + } + + // 2. If leaf is a math function, replace leaf with the internal representation of that math function. + // NOTE: All function tokens at this point should be math functions. + else if (component_value.is_function()) { + auto& function = component_value.function(); + if (function.name().equals_ignoring_ascii_case("calc"sv)) { + auto leaf_calculation = TRY(parse_a_calculation(function.values())); + if (!leaf_calculation) { + parsing_failed_for_child_node = true; + return {}; + } + node = leaf_calculation.release_nonnull(); + } else { + // FIXME: Parse more math functions once we have them. + parsing_failed_for_child_node = true; + return {}; + } + } + + return {}; + })); + + if (parsing_failed_for_child_node) + return nullptr; + + // FIXME: 6. Return the result of simplifying a calculation tree from values. + return single_value.release_value(); } bool Parser::has_ignored_vendor_prefix(StringView string) diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index 76894a6f40..4b8afdde31 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -323,13 +323,7 @@ private: RefPtr parse_grid_template_areas_value(Vector const&); RefPtr parse_grid_area_shorthand_value(Vector const&); - // calc() parsing, according to https://www.w3.org/TR/css-values-3/#calc-syntax - OwnPtr parse_calc_sum(TokenStream&); - OwnPtr parse_calc_product(TokenStream&); - Optional parse_calc_value(TokenStream&); - OwnPtr parse_calc_product_part_with_operator(TokenStream&); - OwnPtr parse_calc_sum_part_with_operator(TokenStream&); - OwnPtr parse_calc_expression(Vector const&); + ErrorOr> parse_a_calculation(Vector const&); ParseErrorOr> parse_complex_selector(TokenStream&, SelectorType); ParseErrorOr> parse_compound_selector(TokenStream&); diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp index 5697510909..a4886502da 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp @@ -12,6 +12,384 @@ namespace Web::CSS { +static bool is_number(CalculatedStyleValue::ResolvedType type) +{ + return type == CalculatedStyleValue::ResolvedType::Number || type == CalculatedStyleValue::ResolvedType::Integer; +} + +static bool is_dimension(CalculatedStyleValue::ResolvedType type) +{ + return type != CalculatedStyleValue::ResolvedType::Number + && type != CalculatedStyleValue::ResolvedType::Integer + && type != CalculatedStyleValue::ResolvedType::Percentage; +} + +CalculationNode::CalculationNode(Type type) + : m_type(type) +{ +} + +CalculationNode::~CalculationNode() = default; + +ErrorOr> NumericCalculationNode::create(NumericValue value) +{ + return adopt_nonnull_own_or_enomem(new (nothrow) NumericCalculationNode(move(value))); +} + +NumericCalculationNode::NumericCalculationNode(NumericValue value) + : CalculationNode(Type::Numeric) + , m_value(move(value)) +{ +} + +NumericCalculationNode::~NumericCalculationNode() = default; + +ErrorOr NumericCalculationNode::to_string() const +{ + return m_value.visit([](auto& value) { return value.to_string(); }); +} + +Optional NumericCalculationNode::resolved_type() const +{ + return m_value.visit( + [](Number const&) { return CalculatedStyleValue::ResolvedType::Number; }, + [](Angle const&) { return CalculatedStyleValue::ResolvedType::Angle; }, + [](Frequency const&) { return CalculatedStyleValue::ResolvedType::Frequency; }, + [](Length const&) { return CalculatedStyleValue::ResolvedType::Length; }, + [](Percentage const&) { return CalculatedStyleValue::ResolvedType::Percentage; }, + [](Time const&) { return CalculatedStyleValue::ResolvedType::Time; }); +} + +bool NumericCalculationNode::contains_percentage() const +{ + return m_value.has(); +} + +CalculatedStyleValue::CalculationResult NumericCalculationNode::resolve(Layout::Node const*, CalculatedStyleValue::PercentageBasis const&) const +{ + return m_value; +} + +ErrorOr NumericCalculationNode::dump(StringBuilder& builder, int indent) const +{ + return builder.try_appendff("{: >{}}NUMERIC({})\n", "", indent, TRY(m_value.visit([](auto& it) { return it.to_string(); }))); +} + +ErrorOr> SumCalculationNode::create(Vector> values) +{ + return adopt_nonnull_own_or_enomem(new (nothrow) SumCalculationNode(move(values))); +} + +SumCalculationNode::SumCalculationNode(Vector> values) + : CalculationNode(Type::Sum) + , m_values(move(values)) +{ + VERIFY(!m_values.is_empty()); +} + +SumCalculationNode::~SumCalculationNode() = default; + +ErrorOr SumCalculationNode::to_string() const +{ + bool first = true; + StringBuilder builder; + for (auto& value : m_values) { + if (!first) + TRY(builder.try_append(" + "sv)); + TRY(builder.try_append(TRY(value->to_string()))); + first = false; + } + return builder.to_string(); +} + +Optional SumCalculationNode::resolved_type() const +{ + // FIXME: Implement https://www.w3.org/TR/css-values-4/#determine-the-type-of-a-calculation + // For now, this is just ad-hoc, based on the old implementation. + + Optional type; + for (auto const& value : m_values) { + auto maybe_value_type = value->resolved_type(); + if (!maybe_value_type.has_value()) + return {}; + auto value_type = maybe_value_type.value(); + + if (!type.has_value()) { + type = value_type; + continue; + } + + // At + or -, check that both sides have the same type, or that one side is a and the other is an . + // If both sides are the same type, resolve to that type. + if (value_type == type) + continue; + + // If one side is a and the other is an , resolve to . + if (is_number(*type) && is_number(value_type)) { + type = CalculatedStyleValue::ResolvedType::Number; + continue; + } + + // FIXME: calc() handles by allowing them to pretend to be whatever type is allowed at this location. + // Since we can't easily check what that type is, we just allow to combine with any other type. + if (type == CalculatedStyleValue::ResolvedType::Percentage && is_dimension(value_type)) { + type = value_type; + continue; + } + if (is_dimension(*type) && value_type == CalculatedStyleValue::ResolvedType::Percentage) + continue; + + return {}; + } + return type; +} + +bool SumCalculationNode::contains_percentage() const +{ + for (auto const& value : m_values) { + if (value->contains_percentage()) + return true; + } + return false; +} + +CalculatedStyleValue::CalculationResult SumCalculationNode::resolve(Layout::Node const* layout_node, CalculatedStyleValue::PercentageBasis const& percentage_basis) const +{ + Optional total; + + for (auto& additional_product : m_values) { + auto additional_value = additional_product->resolve(layout_node, percentage_basis); + if (!total.has_value()) { + total = additional_value; + continue; + } + total->add(additional_value, layout_node, percentage_basis); + } + + return total.value(); +} + +ErrorOr SumCalculationNode::for_each_child_node(Function(NonnullOwnPtr&)> const& callback) +{ + for (auto& item : m_values) { + TRY(item->for_each_child_node(callback)); + TRY(callback(item)); + } + + return {}; +} + +ErrorOr SumCalculationNode::dump(StringBuilder& builder, int indent) const +{ + TRY(builder.try_appendff("{: >{}}SUM:\n", "", indent)); + for (auto const& item : m_values) + TRY(item->dump(builder, indent + 2)); + return {}; +} + +ErrorOr> ProductCalculationNode::create(Vector> values) +{ + return adopt_nonnull_own_or_enomem(new (nothrow) ProductCalculationNode(move(values))); +} + +ProductCalculationNode::ProductCalculationNode(Vector> values) + : CalculationNode(Type::Product) + , m_values(move(values)) +{ + VERIFY(!m_values.is_empty()); +} + +ProductCalculationNode::~ProductCalculationNode() = default; + +ErrorOr ProductCalculationNode::to_string() const +{ + bool first = true; + StringBuilder builder; + for (auto& value : m_values) { + if (!first) + TRY(builder.try_append(" * "sv)); + TRY(builder.try_append(TRY(value->to_string()))); + first = false; + } + return builder.to_string(); +} + +Optional ProductCalculationNode::resolved_type() const +{ + // FIXME: Implement https://www.w3.org/TR/css-values-4/#determine-the-type-of-a-calculation + // For now, this is just ad-hoc, based on the old implementation. + + Optional type; + for (auto const& value : m_values) { + auto maybe_value_type = value->resolved_type(); + if (!maybe_value_type.has_value()) + return {}; + auto value_type = maybe_value_type.value(); + + if (!type.has_value()) { + type = value_type; + continue; + } + + // At *, check that at least one side is . + if (!(is_number(*type) || is_number(value_type))) + return {}; + // If both sides are , resolve to . + if (type == CalculatedStyleValue::ResolvedType::Integer && value_type == CalculatedStyleValue::ResolvedType::Integer) { + type = CalculatedStyleValue::ResolvedType::Integer; + } else { + // Otherwise, resolve to the type of the other side. + if (is_number(*type)) + type = value_type; + } + } + return type; +} + +bool ProductCalculationNode::contains_percentage() const +{ + for (auto const& value : m_values) { + if (value->contains_percentage()) + return true; + } + return false; +} + +CalculatedStyleValue::CalculationResult ProductCalculationNode::resolve(Layout::Node const* layout_node, CalculatedStyleValue::PercentageBasis const& percentage_basis) const +{ + Optional total; + + for (auto& additional_product : m_values) { + auto additional_value = additional_product->resolve(layout_node, percentage_basis); + if (!total.has_value()) { + total = additional_value; + continue; + } + total->multiply_by(additional_value, layout_node); + } + + return total.value(); +} + +ErrorOr ProductCalculationNode::for_each_child_node(Function(NonnullOwnPtr&)> const& callback) +{ + for (auto& item : m_values) { + TRY(item->for_each_child_node(callback)); + TRY(callback(item)); + } + + return {}; +} + +ErrorOr ProductCalculationNode::dump(StringBuilder& builder, int indent) const +{ + TRY(builder.try_appendff("{: >{}}PRODUCT:\n", "", indent)); + for (auto const& item : m_values) + TRY(item->dump(builder, indent + 2)); + return {}; +} + +ErrorOr> NegateCalculationNode::create(NonnullOwnPtr value) +{ + return adopt_nonnull_own_or_enomem(new (nothrow) NegateCalculationNode(move(value))); +} + +NegateCalculationNode::NegateCalculationNode(NonnullOwnPtr value) + : CalculationNode(Type::Negate) + , m_value(move(value)) +{ +} + +NegateCalculationNode::~NegateCalculationNode() = default; + +ErrorOr NegateCalculationNode::to_string() const +{ + return String::formatted("(0 - {})", TRY(m_value->to_string())); +} + +Optional NegateCalculationNode::resolved_type() const +{ + return m_value->resolved_type(); +} + +bool NegateCalculationNode::contains_percentage() const +{ + return m_value->contains_percentage(); +} + +CalculatedStyleValue::CalculationResult NegateCalculationNode::resolve(Layout::Node const* layout_node, CalculatedStyleValue::PercentageBasis const& percentage_basis) const +{ + auto child_value = m_value->resolve(layout_node, percentage_basis); + child_value.negate(); + return child_value; +} + +ErrorOr NegateCalculationNode::for_each_child_node(Function(NonnullOwnPtr&)> const& callback) +{ + TRY(m_value->for_each_child_node(callback)); + TRY(callback(m_value)); + return {}; +} + +ErrorOr NegateCalculationNode::dump(StringBuilder& builder, int indent) const +{ + TRY(builder.try_appendff("{: >{}}NEGATE:\n", "", indent)); + TRY(m_value->dump(builder, indent + 2)); + return {}; +} + +ErrorOr> InvertCalculationNode::create(NonnullOwnPtr value) +{ + return adopt_nonnull_own_or_enomem(new (nothrow) InvertCalculationNode(move(value))); +} + +InvertCalculationNode::InvertCalculationNode(NonnullOwnPtr value) + : CalculationNode(Type::Invert) + , m_value(move(value)) +{ +} + +InvertCalculationNode::~InvertCalculationNode() = default; + +ErrorOr InvertCalculationNode::to_string() const +{ + return String::formatted("(1 / {})", TRY(m_value->to_string())); +} + +Optional InvertCalculationNode::resolved_type() const +{ + auto type = m_value->resolved_type(); + if (type == CalculatedStyleValue::ResolvedType::Integer) + return CalculatedStyleValue::ResolvedType::Number; + return type; +} + +bool InvertCalculationNode::contains_percentage() const +{ + return m_value->contains_percentage(); +} + +CalculatedStyleValue::CalculationResult InvertCalculationNode::resolve(Layout::Node const* layout_node, CalculatedStyleValue::PercentageBasis const& percentage_basis) const +{ + auto child_value = m_value->resolve(layout_node, percentage_basis); + child_value.invert(); + return child_value; +} + +ErrorOr InvertCalculationNode::for_each_child_node(Function(NonnullOwnPtr&)> const& callback) +{ + TRY(m_value->for_each_child_node(callback)); + TRY(callback(m_value)); + return {}; +} + +ErrorOr InvertCalculationNode::dump(StringBuilder& builder, int indent) const +{ + TRY(builder.try_appendff("{: >{}}INVERT:\n", "", indent)); + TRY(m_value->dump(builder, indent + 2)); + return {}; +} + void CalculatedStyleValue::CalculationResult::add(CalculationResult const& other, Layout::Node const* layout_node, PercentageBasis const& percentage_basis) { add_or_subtract_internal(SumOperation::Add, other, layout_node, percentage_basis); @@ -201,9 +579,57 @@ void CalculatedStyleValue::CalculationResult::divide_by(CalculationResult const& }); } +void CalculatedStyleValue::CalculationResult::negate() +{ + m_value.visit( + [&](Number const& number) { + m_value = Number { number.type(), 1 - number.value() }; + }, + [&](Angle const& angle) { + m_value = Angle { 1 - angle.raw_value(), angle.type() }; + }, + [&](Frequency const& frequency) { + m_value = Frequency { 1 - frequency.raw_value(), frequency.type() }; + }, + [&](Length const& length) { + m_value = Length { 1 - length.raw_value(), length.type() }; + }, + [&](Time const& time) { + m_value = Time { 1 - time.raw_value(), time.type() }; + }, + [&](Percentage const& percentage) { + m_value = Percentage { 1 - percentage.value() }; + }); +} + +void CalculatedStyleValue::CalculationResult::invert() +{ + // FIXME: Correctly handle division by zero. + m_value.visit( + [&](Number const& number) { + m_value = Number { Number::Type::Number, 1 / number.value() }; + }, + [&](Angle const& angle) { + m_value = Angle { 1 / angle.raw_value(), angle.type() }; + }, + [&](Frequency const& frequency) { + m_value = Frequency { 1 / frequency.raw_value(), frequency.type() }; + }, + [&](Length const& length) { + m_value = Length { 1 / length.raw_value(), length.type() }; + }, + [&](Time const& time) { + m_value = Time { 1 / time.raw_value(), time.type() }; + }, + [&](Percentage const& percentage) { + m_value = Percentage { 1 / percentage.value() }; + }); +} + ErrorOr CalculatedStyleValue::to_string() const { - return String::formatted("calc({})", TRY(m_expression->to_string())); + // FIXME: Implement this according to https://www.w3.org/TR/css-values-4/#calc-serialize once that stabilizes. + return String::formatted("calc({})", TRY(m_calculation->to_string())); } bool CalculatedStyleValue::equals(StyleValue const& other) const @@ -214,45 +640,9 @@ bool CalculatedStyleValue::equals(StyleValue const& other) const return to_string().release_value_but_fixme_should_propagate_errors() == other.to_string().release_value_but_fixme_should_propagate_errors(); } -ErrorOr CalculatedStyleValue::CalcValue::to_string() const -{ - return value.visit( - [](Number const& number) -> ErrorOr { return String::number(number.value()); }, - [](NonnullOwnPtr const& sum) -> ErrorOr { return String::formatted("({})", TRY(sum->to_string())); }, - [](auto const& v) -> ErrorOr { return v.to_string(); }); -} - -ErrorOr CalculatedStyleValue::CalcSum::to_string() const -{ - StringBuilder builder; - TRY(builder.try_append(TRY(first_calc_product->to_string()))); - for (auto const& item : zero_or_more_additional_calc_products) - TRY(builder.try_append(TRY(item->to_string()))); - return builder.to_string(); -} - -ErrorOr CalculatedStyleValue::CalcProduct::to_string() const -{ - StringBuilder builder; - TRY(builder.try_append(TRY(first_calc_value.to_string()))); - for (auto const& item : zero_or_more_additional_calc_values) - TRY(builder.try_append(TRY(item->to_string()))); - return builder.to_string(); -} - -ErrorOr CalculatedStyleValue::CalcSumPartWithOperator::to_string() const -{ - return String::formatted(" {} {}", op == SumOperation::Add ? "+"sv : "-"sv, TRY(value->to_string())); -} - -ErrorOr CalculatedStyleValue::CalcProductPartWithOperator::to_string() const -{ - return String::formatted(" {} {}", op == ProductOperation::Multiply ? "*"sv : "/"sv, TRY(value.to_string())); -} - Optional CalculatedStyleValue::resolve_angle() const { - auto result = m_expression->resolve(nullptr, {}); + auto result = m_calculation->resolve(nullptr, {}); if (result.value().has()) return result.value().get(); @@ -261,7 +651,7 @@ Optional CalculatedStyleValue::resolve_angle() const Optional CalculatedStyleValue::resolve_angle_percentage(Angle const& percentage_basis) const { - auto result = m_expression->resolve(nullptr, percentage_basis); + auto result = m_calculation->resolve(nullptr, percentage_basis); return result.value().visit( [&](Angle const& angle) -> Optional { @@ -277,7 +667,7 @@ Optional CalculatedStyleValue::resolve_angle_percentage(Angle const& perc Optional CalculatedStyleValue::resolve_frequency() const { - auto result = m_expression->resolve(nullptr, {}); + auto result = m_calculation->resolve(nullptr, {}); if (result.value().has()) return result.value().get(); @@ -286,7 +676,7 @@ Optional CalculatedStyleValue::resolve_frequency() const Optional CalculatedStyleValue::resolve_frequency_percentage(Frequency const& percentage_basis) const { - auto result = m_expression->resolve(nullptr, percentage_basis); + auto result = m_calculation->resolve(nullptr, percentage_basis); return result.value().visit( [&](Frequency const& frequency) -> Optional { @@ -302,7 +692,7 @@ Optional CalculatedStyleValue::resolve_frequency_percentage(Frequency Optional CalculatedStyleValue::resolve_length(Layout::Node const& layout_node) const { - auto result = m_expression->resolve(&layout_node, {}); + auto result = m_calculation->resolve(&layout_node, {}); if (result.value().has()) return result.value().get(); @@ -311,7 +701,7 @@ Optional CalculatedStyleValue::resolve_length(Layout::Node const& layout Optional CalculatedStyleValue::resolve_length_percentage(Layout::Node const& layout_node, Length const& percentage_basis) const { - auto result = m_expression->resolve(&layout_node, percentage_basis); + auto result = m_calculation->resolve(&layout_node, percentage_basis); return result.value().visit( [&](Length const& length) -> Optional { @@ -327,7 +717,7 @@ Optional CalculatedStyleValue::resolve_length_percentage(Layout::Node co Optional CalculatedStyleValue::resolve_percentage() const { - auto result = m_expression->resolve(nullptr, {}); + auto result = m_calculation->resolve(nullptr, {}); if (result.value().has()) return result.value().get(); return {}; @@ -335,7 +725,7 @@ Optional CalculatedStyleValue::resolve_percentage() const Optional