| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/css/parser/sizes_math_function_parser.h" |
| |
| #include "third_party/blink/renderer/core/css/media_values.h" |
| #include "third_party/blink/renderer/core/css/parser/css_parser_token.h" |
| #include "third_party/blink/renderer/core/css_value_keywords.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| |
| namespace blink { |
| |
| SizesMathFunctionParser::SizesMathFunctionParser(CSSParserTokenRange range, |
| MediaValues* media_values) |
| : media_values_(media_values), result_(0) { |
| is_valid_ = CalcToReversePolishNotation(range) && Calculate(); |
| } |
| |
| float SizesMathFunctionParser::Result() const { |
| DCHECK(is_valid_); |
| return result_; |
| } |
| |
| static bool OperatorPriority(CSSMathOperator cc, bool& high_priority) { |
| if (cc == CSSMathOperator::kAdd || cc == CSSMathOperator::kSubtract) |
| high_priority = false; |
| else if (cc == CSSMathOperator::kMultiply || cc == CSSMathOperator::kDivide) |
| high_priority = true; |
| else |
| return false; |
| return true; |
| } |
| |
| bool SizesMathFunctionParser::HandleOperator(Vector<CSSParserToken>& stack, |
| const CSSParserToken& token) { |
| // If the token is not an operator, then return. Else determine the |
| // precedence of the new operator (op1). |
| bool incoming_operator_priority; |
| if (!OperatorPriority(ParseCSSArithmeticOperator(token), |
| incoming_operator_priority)) |
| return false; |
| |
| while (!stack.IsEmpty()) { |
| // While there is an operator (op2) at the top of the stack, |
| // determine its precedence, and... |
| const CSSParserToken& top_of_stack = stack.back(); |
| if (top_of_stack.GetType() != kDelimiterToken) |
| break; |
| bool stack_operator_priority; |
| if (!OperatorPriority(ParseCSSArithmeticOperator(top_of_stack), |
| stack_operator_priority)) |
| return false; |
| // ...if op1 is left-associative (all currently supported |
| // operators are) and its precedence is equal to that of op2, or |
| // op1 has precedence less than that of op2, ... |
| if (incoming_operator_priority && !stack_operator_priority) |
| break; |
| // ...pop op2 off the stack and add it to the output queue. |
| AppendOperator(top_of_stack); |
| stack.pop_back(); |
| } |
| // Push op1 onto the stack. |
| stack.push_back(token); |
| return true; |
| } |
| |
| bool SizesMathFunctionParser::HandleRightParenthesis( |
| Vector<CSSParserToken>& stack) { |
| // If the token is a right parenthesis: |
| // Until the token at the top of the stack is a left parenthesis or a |
| // function, pop operators off the stack onto the output queue. |
| // Also count the number of commas to get the number of function |
| // parameters if this right parenthesis closes a function. |
| wtf_size_t comma_count = 0; |
| while (!stack.IsEmpty() && stack.back().GetType() != kLeftParenthesisToken && |
| stack.back().GetType() != kFunctionToken) { |
| if (stack.back().GetType() == kCommaToken) |
| ++comma_count; |
| else |
| AppendOperator(stack.back()); |
| stack.pop_back(); |
| } |
| // If the stack runs out without finding a left parenthesis, then there |
| // are mismatched parentheses. |
| if (stack.IsEmpty()) |
| return false; |
| |
| CSSParserToken left_side = stack.back(); |
| stack.pop_back(); |
| |
| if (left_side.GetType() == kLeftParenthesisToken || |
| left_side.FunctionId() == CSSValueID::kCalc) { |
| // There should be exactly one calculation within calc() or parentheses. |
| return !comma_count; |
| } |
| |
| if (left_side.FunctionId() == CSSValueID::kClamp) { |
| if (comma_count != 2) |
| return false; |
| // Convert clamp(MIN, VAL, MAX) into max(MIN, min(VAL, MAX)) |
| // https://www.w3.org/TR/css-values-4/#calc-notation |
| value_list_.emplace_back(CSSMathOperator::kMin); |
| value_list_.emplace_back(CSSMathOperator::kMax); |
| return true; |
| } |
| |
| // Break variadic min/max() into binary operations to fit in the reverse |
| // polish notation. |
| CSSMathOperator op = left_side.FunctionId() == CSSValueID::kMin |
| ? CSSMathOperator::kMin |
| : CSSMathOperator::kMax; |
| for (wtf_size_t i = 0; i < comma_count; ++i) |
| value_list_.emplace_back(op); |
| return true; |
| } |
| |
| bool SizesMathFunctionParser::HandleComma(Vector<CSSParserToken>& stack, |
| const CSSParserToken& token) { |
| // Treat comma as a binary right-associative operation for now, so that |
| // when reaching the right parenthesis of the function, we can get the |
| // number of parameters by counting the number of commas. |
| while (!stack.IsEmpty() && stack.back().GetType() != kFunctionToken && |
| stack.back().GetType() != kLeftParenthesisToken && |
| stack.back().GetType() != kCommaToken) { |
| AppendOperator(stack.back()); |
| stack.pop_back(); |
| } |
| // Commas are allowed as function parameter separators only |
| if (stack.IsEmpty() || stack.back().GetType() == kLeftParenthesisToken) |
| return false; |
| stack.push_back(token); |
| return true; |
| } |
| |
| void SizesMathFunctionParser::AppendNumber(const CSSParserToken& token) { |
| SizesMathValue value; |
| value.value = token.NumericValue(); |
| value_list_.push_back(value); |
| } |
| |
| bool SizesMathFunctionParser::AppendLength(const CSSParserToken& token) { |
| SizesMathValue value; |
| double result = 0; |
| if (!media_values_->ComputeLength(token.NumericValue(), token.GetUnitType(), |
| result)) |
| return false; |
| value.value = result; |
| value.is_length = true; |
| value_list_.push_back(value); |
| return true; |
| } |
| |
| void SizesMathFunctionParser::AppendOperator(const CSSParserToken& token) { |
| value_list_.emplace_back(ParseCSSArithmeticOperator(token)); |
| } |
| |
| bool SizesMathFunctionParser::CalcToReversePolishNotation( |
| CSSParserTokenRange range) { |
| // This method implements the shunting yard algorithm, to turn the calc syntax |
| // into a reverse polish notation. |
| // http://en.wikipedia.org/wiki/Shunting-yard_algorithm |
| |
| Vector<CSSParserToken> stack; |
| while (!range.AtEnd()) { |
| const CSSParserToken& token = range.Consume(); |
| switch (token.GetType()) { |
| case kNumberToken: |
| AppendNumber(token); |
| break; |
| case kDimensionToken: |
| if (!CSSPrimitiveValue::IsLength(token.GetUnitType()) || |
| !AppendLength(token)) |
| return false; |
| break; |
| case kDelimiterToken: |
| if (!HandleOperator(stack, token)) |
| return false; |
| break; |
| case kFunctionToken: |
| if (token.FunctionId() == CSSValueID::kMin || |
| token.FunctionId() == CSSValueID::kMax || |
| token.FunctionId() == CSSValueID::kClamp) { |
| stack.push_back(token); |
| break; |
| } |
| if (token.FunctionId() != CSSValueID::kCalc) |
| return false; |
| // "calc(" is the same as "(" |
| FALLTHROUGH; |
| case kLeftParenthesisToken: |
| // If the token is a left parenthesis, then push it onto the stack. |
| stack.push_back(token); |
| break; |
| case kRightParenthesisToken: |
| if (!HandleRightParenthesis(stack)) |
| return false; |
| break; |
| case kCommaToken: |
| if (!HandleComma(stack, token)) |
| return false; |
| break; |
| case kWhitespaceToken: |
| case kEOFToken: |
| break; |
| case kCommentToken: |
| NOTREACHED(); |
| FALLTHROUGH; |
| case kCDOToken: |
| case kCDCToken: |
| case kAtKeywordToken: |
| case kHashToken: |
| case kUrlToken: |
| case kBadUrlToken: |
| case kPercentageToken: |
| case kIncludeMatchToken: |
| case kDashMatchToken: |
| case kPrefixMatchToken: |
| case kSuffixMatchToken: |
| case kSubstringMatchToken: |
| case kColumnToken: |
| case kUnicodeRangeToken: |
| case kIdentToken: |
| case kColonToken: |
| case kSemicolonToken: |
| case kLeftBraceToken: |
| case kLeftBracketToken: |
| case kRightBraceToken: |
| case kRightBracketToken: |
| case kStringToken: |
| case kBadStringToken: |
| return false; |
| } |
| } |
| |
| // When there are no more tokens to read: |
| // While there are still operator tokens in the stack: |
| while (!stack.IsEmpty()) { |
| // If the operator token on the top of the stack is a parenthesis, then |
| // there are unclosed parentheses. |
| CSSParserTokenType type = stack.back().GetType(); |
| if (type != kLeftParenthesisToken && type != kFunctionToken) { |
| // Pop the operator onto the output queue. |
| AppendOperator(stack.back()); |
| } |
| stack.pop_back(); |
| } |
| return true; |
| } |
| |
| static bool OperateOnStack(Vector<SizesMathValue>& stack, |
| CSSMathOperator operation) { |
| if (stack.size() < 2) |
| return false; |
| SizesMathValue right_operand = stack.back(); |
| stack.pop_back(); |
| SizesMathValue left_operand = stack.back(); |
| stack.pop_back(); |
| bool is_length; |
| switch (operation) { |
| case CSSMathOperator::kAdd: |
| if (right_operand.is_length != left_operand.is_length) |
| return false; |
| is_length = (right_operand.is_length && left_operand.is_length); |
| stack.push_back( |
| SizesMathValue(left_operand.value + right_operand.value, is_length)); |
| break; |
| case CSSMathOperator::kSubtract: |
| if (right_operand.is_length != left_operand.is_length) |
| return false; |
| is_length = (right_operand.is_length && left_operand.is_length); |
| stack.push_back( |
| SizesMathValue(left_operand.value - right_operand.value, is_length)); |
| break; |
| case CSSMathOperator::kMultiply: |
| if (right_operand.is_length && left_operand.is_length) |
| return false; |
| is_length = (right_operand.is_length || left_operand.is_length); |
| stack.push_back( |
| SizesMathValue(left_operand.value * right_operand.value, is_length)); |
| break; |
| case CSSMathOperator::kDivide: |
| if (right_operand.is_length || right_operand.value == 0) |
| return false; |
| stack.push_back(SizesMathValue(left_operand.value / right_operand.value, |
| left_operand.is_length)); |
| break; |
| case CSSMathOperator::kMin: |
| if (right_operand.is_length != left_operand.is_length) |
| return false; |
| is_length = (right_operand.is_length && left_operand.is_length); |
| stack.push_back(SizesMathValue( |
| std::min(left_operand.value, right_operand.value), is_length)); |
| break; |
| case CSSMathOperator::kMax: |
| if (right_operand.is_length != left_operand.is_length) |
| return false; |
| is_length = (right_operand.is_length && left_operand.is_length); |
| stack.push_back(SizesMathValue( |
| std::max(left_operand.value, right_operand.value), is_length)); |
| break; |
| default: |
| return false; |
| } |
| return true; |
| } |
| |
| bool SizesMathFunctionParser::Calculate() { |
| Vector<SizesMathValue> stack; |
| for (const auto& value : value_list_) { |
| if (value.operation == CSSMathOperator::kInvalid) { |
| stack.push_back(value); |
| } else { |
| if (!OperateOnStack(stack, value.operation)) |
| return false; |
| } |
| } |
| if (stack.size() == 1 && stack.back().is_length) { |
| result_ = std::max(clampTo<float>(stack.back().value), (float)0.0); |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace blink |