blob: 56a1526b9e0cf40674513d0413227faee1101033 [file] [log] [blame]
// Copyright 2017 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/cssom/css_numeric_value.h"
#include <numeric>
#include "third_party/blink/renderer/bindings/core/v8/v8_css_numeric_type.h"
#include "third_party/blink/renderer/core/css/css_math_expression_node.h"
#include "third_party/blink/renderer/core/css/css_math_function_value.h"
#include "third_party/blink/renderer/core/css/css_primitive_value.h"
#include "third_party/blink/renderer/core/css/cssom/css_math_invert.h"
#include "third_party/blink/renderer/core/css/cssom/css_math_max.h"
#include "third_party/blink/renderer/core/css/cssom/css_math_min.h"
#include "third_party/blink/renderer/core/css/cssom/css_math_negate.h"
#include "third_party/blink/renderer/core/css/cssom/css_math_product.h"
#include "third_party/blink/renderer/core/css/cssom/css_math_sum.h"
#include "third_party/blink/renderer/core/css/cssom/css_unit_value.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_token_stream.h"
#include "third_party/blink/renderer/core/css/parser/css_tokenizer.h"
#include "third_party/blink/renderer/core/css_value_keywords.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
namespace blink {
namespace {
template <CSSStyleValue::StyleValueType type>
void PrependValueForArithmetic(CSSNumericValueVector& vector,
CSSNumericValue* value) {
DCHECK(value);
if (value->GetType() == type)
vector.PrependVector(static_cast<CSSMathVariadic*>(value)->NumericValues());
else
vector.push_front(value);
}
template <class BinaryOperation>
CSSUnitValue* MaybeSimplifyAsUnitValue(const CSSNumericValueVector& values,
const BinaryOperation& op) {
DCHECK(!values.IsEmpty());
auto* first_unit_value = DynamicTo<CSSUnitValue>(values[0].Get());
if (!first_unit_value)
return nullptr;
double final_value = first_unit_value->value();
for (wtf_size_t i = 1; i < values.size(); i++) {
auto* unit_value = DynamicTo<CSSUnitValue>(values[i].Get());
if (!unit_value ||
unit_value->GetInternalUnit() != first_unit_value->GetInternalUnit())
return nullptr;
final_value = op(final_value, unit_value->value());
}
return CSSUnitValue::Create(final_value, first_unit_value->GetInternalUnit());
}
CSSUnitValue* MaybeMultiplyAsUnitValue(const CSSNumericValueVector& values) {
DCHECK(!values.IsEmpty());
// We are allowed one unit value with type other than kNumber.
auto unit_other_than_number = CSSPrimitiveValue::UnitType::kNumber;
double final_value = 1.0;
for (wtf_size_t i = 0; i < values.size(); i++) {
auto* unit_value = DynamicTo<CSSUnitValue>(values[i].Get());
if (!unit_value)
return nullptr;
if (unit_value->GetInternalUnit() != CSSPrimitiveValue::UnitType::kNumber) {
if (unit_other_than_number != CSSPrimitiveValue::UnitType::kNumber)
return nullptr;
unit_other_than_number = unit_value->GetInternalUnit();
}
final_value *= unit_value->value();
}
return CSSUnitValue::Create(final_value, unit_other_than_number);
}
CSSMathOperator CanonicalOperator(CSSMathOperator op) {
if (op == CSSMathOperator::kAdd || op == CSSMathOperator::kSubtract)
return CSSMathOperator::kAdd;
return CSSMathOperator::kMultiply;
}
bool CanCombineNodes(const CSSMathExpressionNode& root,
const CSSMathExpressionNode& node) {
DCHECK(root.IsBinaryOperation());
if (!node.IsBinaryOperation())
return false;
if (node.IsNestedCalc())
return false;
return CanonicalOperator(
To<CSSMathExpressionBinaryOperation>(root).OperatorType()) ==
CanonicalOperator(
To<CSSMathExpressionBinaryOperation>(node).OperatorType());
}
CSSNumericValue* NegateOrInvertIfRequired(CSSMathOperator parent_op,
CSSNumericValue* value) {
DCHECK(value);
if (parent_op == CSSMathOperator::kSubtract)
return CSSMathNegate::Create(value);
if (parent_op == CSSMathOperator::kDivide)
return CSSMathInvert::Create(value);
return value;
}
CSSNumericValue* CalcToNumericValue(const CSSMathExpressionNode& root) {
if (root.IsNumericLiteral()) {
const CSSPrimitiveValue::UnitType unit = root.ResolvedUnitType();
auto* value = CSSUnitValue::Create(
root.DoubleValue(), unit == CSSPrimitiveValue::UnitType::kInteger
? CSSPrimitiveValue::UnitType::kNumber
: unit);
DCHECK(value);
// For cases like calc(1), we need to wrap the value in a CSSMathSum
if (!root.IsNestedCalc())
return value;
CSSNumericValueVector values;
values.push_back(value);
return CSSMathSum::Create(std::move(values));
}
CSSNumericValueVector values;
// When the node is a variadic operation, we return either a CSSMathMin or a
// CSSMathMax.
if (root.IsVariadicOperation()) {
const auto& node = To<CSSMathExpressionVariadicOperation>(root);
for (const auto& operand : node.GetOperands())
values.push_back(CalcToNumericValue(*operand));
if (node.OperatorType() == CSSMathOperator::kMin)
return CSSMathMin::Create(std::move(values));
DCHECK(node.OperatorType() == CSSMathOperator::kMax);
return CSSMathMax::Create(std::move(values));
}
DCHECK(root.IsBinaryOperation());
// When the node is a binary operator, we return either a CSSMathSum or a
// CSSMathProduct.
// For cases like calc(1 + 2 + 3), the calc expression tree looks like:
// + //
// / \ //
// + 3 //
// / \ //
// 1 2 //
//
// But we want to produce a CSSMathValue tree that looks like:
// + //
// /|\ //
// 1 2 3 //
//
// So when the left child has the same operator as its parent, we can combine
// the two nodes. We keep moving down the left side of the tree as long as the
// current node and the root can be combined, collecting the right child of
// the nodes that we encounter.
const CSSMathExpressionNode* cur_node = &root;
do {
DCHECK(cur_node->IsBinaryOperation());
const CSSMathExpressionBinaryOperation* binary_op =
To<CSSMathExpressionBinaryOperation>(cur_node);
DCHECK(binary_op->LeftExpressionNode());
DCHECK(binary_op->RightExpressionNode());
auto* const value = CalcToNumericValue(*binary_op->RightExpressionNode());
// If the current node is a '-' or '/', it's really just a '+' or '*' with
// the right child negated or inverted, respectively.
values.push_back(
NegateOrInvertIfRequired(binary_op->OperatorType(), value));
cur_node = binary_op->LeftExpressionNode();
} while (CanCombineNodes(root, *cur_node));
DCHECK(cur_node);
values.push_back(CalcToNumericValue(*cur_node));
// Our algorithm collects the children in reverse order, so we have to reverse
// the values.
std::reverse(values.begin(), values.end());
CSSMathOperator operator_type =
To<CSSMathExpressionBinaryOperation>(root).OperatorType();
if (operator_type == CSSMathOperator::kAdd ||
operator_type == CSSMathOperator::kSubtract)
return CSSMathSum::Create(std::move(values));
return CSSMathProduct::Create(std::move(values));
}
CSSUnitValue* CSSNumericSumValueEntryToUnitValue(
const CSSNumericSumValue::Term& term) {
if (term.units.size() == 0)
return CSSUnitValue::Create(term.value);
if (term.units.size() == 1 && term.units.begin()->value == 1)
return CSSUnitValue::Create(term.value, term.units.begin()->key);
return nullptr;
}
} // namespace
bool CSSNumericValue::IsValidUnit(CSSPrimitiveValue::UnitType unit) {
// UserUnits returns true for CSSPrimitiveValue::IsLength below.
if (unit == CSSPrimitiveValue::UnitType::kUserUnits)
return false;
if (unit == CSSPrimitiveValue::UnitType::kNumber ||
unit == CSSPrimitiveValue::UnitType::kPercentage ||
CSSPrimitiveValue::IsLength(unit) || CSSPrimitiveValue::IsAngle(unit) ||
CSSPrimitiveValue::IsTime(unit) || CSSPrimitiveValue::IsFrequency(unit) ||
CSSPrimitiveValue::IsResolution(unit) || CSSPrimitiveValue::IsFlex(unit))
return true;
return false;
}
CSSPrimitiveValue::UnitType CSSNumericValue::UnitFromName(const String& name) {
if (name.IsEmpty())
return CSSPrimitiveValue::UnitType::kUnknown;
if (EqualIgnoringASCIICase(name, "number"))
return CSSPrimitiveValue::UnitType::kNumber;
if (EqualIgnoringASCIICase(name, "percent") || name == "%")
return CSSPrimitiveValue::UnitType::kPercentage;
return CSSPrimitiveValue::StringToUnitType(name);
}
CSSNumericValue* CSSNumericValue::parse(const String& css_text,
ExceptionState& exception_state) {
CSSTokenizer tokenizer(css_text);
CSSParserTokenStream stream(tokenizer);
stream.ConsumeWhitespace();
auto range = stream.ConsumeUntilPeekedTypeIs<>();
stream.ConsumeWhitespace();
if (!stream.AtEnd()) {
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"Invalid math expression");
return nullptr;
}
switch (range.Peek().GetType()) {
case kNumberToken:
case kPercentageToken:
case kDimensionToken: {
const auto token = range.ConsumeIncludingWhitespace();
if (!range.AtEnd())
break;
return CSSUnitValue::Create(token.NumericValue(), token.GetUnitType());
}
case kFunctionToken:
if (range.Peek().FunctionId() == CSSValueID::kCalc ||
range.Peek().FunctionId() == CSSValueID::kWebkitCalc ||
range.Peek().FunctionId() == CSSValueID::kMin ||
range.Peek().FunctionId() == CSSValueID::kMax ||
range.Peek().FunctionId() == CSSValueID::kClamp) {
CSSMathExpressionNode* expression =
CSSMathExpressionNode::ParseCalc(range);
if (expression)
return CalcToNumericValue(*expression);
}
break;
default:
break;
}
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"Invalid math expression");
return nullptr;
}
CSSNumericValue* CSSNumericValue::FromCSSValue(const CSSPrimitiveValue& value) {
if (value.IsCalculated()) {
return CalcToNumericValue(
*To<CSSMathFunctionValue>(value).ExpressionNode());
}
return CSSUnitValue::FromCSSValue(To<CSSNumericLiteralValue>(value));
}
/* static */
CSSNumericValue* CSSNumericValue::FromNumberish(const CSSNumberish& value) {
if (value.IsDouble()) {
return CSSUnitValue::Create(value.GetAsDouble(),
CSSPrimitiveValue::UnitType::kNumber);
}
return value.GetAsCSSNumericValue();
}
CSSUnitValue* CSSNumericValue::to(const String& unit_string,
ExceptionState& exception_state) {
CSSPrimitiveValue::UnitType target_unit = UnitFromName(unit_string);
if (!IsValidUnit(target_unit)) {
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"Invalid unit for conversion");
return nullptr;
}
CSSUnitValue* result = to(target_unit);
if (!result) {
exception_state.ThrowTypeError("Cannot convert to " + unit_string);
return nullptr;
}
return result;
}
CSSUnitValue* CSSNumericValue::to(CSSPrimitiveValue::UnitType unit) const {
const auto sum = SumValue();
if (!sum || sum->terms.size() != 1)
return nullptr;
CSSUnitValue* value = CSSNumericSumValueEntryToUnitValue(sum->terms[0]);
if (!value)
return nullptr;
return value->ConvertTo(unit);
}
CSSMathSum* CSSNumericValue::toSum(const Vector<String>& unit_strings,
ExceptionState& exception_state) {
for (const auto& unit_string : unit_strings) {
if (!IsValidUnit(UnitFromName(unit_string))) {
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"Invalid unit for conversion");
return nullptr;
}
}
const base::Optional<CSSNumericSumValue> sum = SumValue();
if (!sum) {
exception_state.ThrowTypeError("Invalid value for conversion");
return nullptr;
}
CSSNumericValueVector values;
for (const auto& term : sum->terms) {
CSSUnitValue* value = CSSNumericSumValueEntryToUnitValue(term);
if (!value) {
exception_state.ThrowTypeError("Invalid value for conversion");
return nullptr;
}
values.push_back(value);
}
if (unit_strings.size() == 0) {
std::sort(values.begin(), values.end(), [](const auto& a, const auto& b) {
return WTF::CodeUnitCompareLessThan(To<CSSUnitValue>(a.Get())->unit(),
To<CSSUnitValue>(b.Get())->unit());
});
// We got 'values' from a sum value, so it must be a valid CSSMathSum.
CSSMathSum* result = CSSMathSum::Create(values);
DCHECK(result);
return result;
}
CSSNumericValueVector result;
for (const auto& unit_string : unit_strings) {
CSSPrimitiveValue::UnitType target_unit = UnitFromName(unit_string);
DCHECK(IsValidUnit(target_unit));
// Collect all the terms that are compatible with this unit.
// We mark used terms as null so we don't use them again.
double total_value =
std::accumulate(values.begin(), values.end(), 0.0,
[target_unit](double cur_sum, auto& value) {
if (value) {
auto& unit_value = To<CSSUnitValue>(*value);
if (const auto* converted_value =
unit_value.ConvertTo(target_unit)) {
cur_sum += converted_value->value();
value = nullptr;
}
}
return cur_sum;
});
result.push_back(CSSUnitValue::Create(total_value, target_unit));
}
if (std::any_of(values.begin(), values.end(),
[](const auto& v) { return v; })) {
exception_state.ThrowTypeError(
"There were leftover terms that were not converted");
return nullptr;
}
CSSMathSum* value = CSSMathSum::Create(result);
if (!value) {
exception_state.ThrowTypeError("Can't create CSSMathSum");
return nullptr;
}
return value;
}
CSSNumericType* CSSNumericValue::type() const {
CSSNumericType* type = CSSNumericType::Create();
using BaseType = CSSNumericValueType::BaseType;
if (int exponent = type_.Exponent(BaseType::kLength))
type->setLength(exponent);
if (int exponent = type_.Exponent(BaseType::kAngle))
type->setAngle(exponent);
if (int exponent = type_.Exponent(BaseType::kTime))
type->setTime(exponent);
if (int exponent = type_.Exponent(BaseType::kFrequency))
type->setFrequency(exponent);
if (int exponent = type_.Exponent(BaseType::kResolution))
type->setResolution(exponent);
if (int exponent = type_.Exponent(BaseType::kFlex))
type->setFlex(exponent);
if (int exponent = type_.Exponent(BaseType::kPercent))
type->setPercent(exponent);
if (type_.HasPercentHint()) {
type->setPercentHint(
CSSNumericValueType::BaseTypeToString(type_.PercentHint()));
}
return type;
}
CSSNumericValue* CSSNumericValue::add(
const HeapVector<CSSNumberish>& numberishes,
ExceptionState& exception_state) {
auto values = CSSNumberishesToNumericValues(numberishes);
PrependValueForArithmetic<kSumType>(values, this);
if (CSSUnitValue* unit_value =
MaybeSimplifyAsUnitValue(values, std::plus<double>())) {
return unit_value;
}
return CSSMathSum::Create(std::move(values));
}
CSSNumericValue* CSSNumericValue::sub(
const HeapVector<CSSNumberish>& numberishes,
ExceptionState& exception_state) {
auto values = CSSNumberishesToNumericValues(numberishes);
std::transform(values.begin(), values.end(), values.begin(),
[](CSSNumericValue* v) { return v->Negate(); });
PrependValueForArithmetic<kSumType>(values, this);
if (CSSUnitValue* unit_value =
MaybeSimplifyAsUnitValue(values, std::plus<double>())) {
return unit_value;
}
return CSSMathSum::Create(std::move(values));
}
CSSNumericValue* CSSNumericValue::mul(
const HeapVector<CSSNumberish>& numberishes,
ExceptionState& exception_state) {
auto values = CSSNumberishesToNumericValues(numberishes);
PrependValueForArithmetic<kProductType>(values, this);
if (CSSUnitValue* unit_value = MaybeMultiplyAsUnitValue(values))
return unit_value;
return CSSMathProduct::Create(std::move(values));
}
CSSNumericValue* CSSNumericValue::div(
const HeapVector<CSSNumberish>& numberishes,
ExceptionState& exception_state) {
auto values = CSSNumberishesToNumericValues(numberishes);
for (auto& v : values) {
auto* invert_value = v->Invert();
if (!invert_value) {
exception_state.ThrowRangeError("Can't divide-by-zero");
return nullptr;
}
v = invert_value;
}
PrependValueForArithmetic<kProductType>(values, this);
if (CSSUnitValue* unit_value = MaybeMultiplyAsUnitValue(values))
return unit_value;
return CSSMathProduct::Create(std::move(values));
}
CSSNumericValue* CSSNumericValue::min(
const HeapVector<CSSNumberish>& numberishes,
ExceptionState& exception_state) {
auto values = CSSNumberishesToNumericValues(numberishes);
PrependValueForArithmetic<kMinType>(values, this);
if (CSSUnitValue* unit_value = MaybeSimplifyAsUnitValue(
values, [](double a, double b) { return std::min(a, b); })) {
return unit_value;
}
return CSSMathMin::Create(std::move(values));
}
CSSNumericValue* CSSNumericValue::max(
const HeapVector<CSSNumberish>& numberishes,
ExceptionState& exception_state) {
auto values = CSSNumberishesToNumericValues(numberishes);
PrependValueForArithmetic<kMaxType>(values, this);
if (CSSUnitValue* unit_value = MaybeSimplifyAsUnitValue(
values, [](double a, double b) { return std::max(a, b); })) {
return unit_value;
}
return CSSMathMax::Create(std::move(values));
}
bool CSSNumericValue::equals(const HeapVector<CSSNumberish>& args) {
CSSNumericValueVector values = CSSNumberishesToNumericValues(args);
return std::all_of(values.begin(), values.end(),
[this](const auto& v) { return this->Equals(*v); });
}
String CSSNumericValue::toString() const {
StringBuilder result;
BuildCSSText(Nested::kNo, ParenLess::kNo, result);
return result.ToString();
}
CSSNumericValue* CSSNumericValue::Negate() {
return CSSMathNegate::Create(this);
}
CSSNumericValue* CSSNumericValue::Invert() {
return CSSMathInvert::Create(this);
}
CSSNumericValueVector CSSNumberishesToNumericValues(
const HeapVector<CSSNumberish>& values) {
CSSNumericValueVector result;
for (const CSSNumberish& value : values) {
result.push_back(CSSNumericValue::FromNumberish(value));
}
return result;
}
} // namespace blink