blob: e548fc1e386c49ea5192b427dcacd3436517ec15 [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 "core/css/cssom/CSSNumericValue.h"
#include "bindings/core/v8/ExceptionState.h"
#include "core/css/CSSCalculationValue.h"
#include "core/css/CSSPrimitiveValue.h"
#include "core/css/cssom/CSSMathInvert.h"
#include "core/css/cssom/CSSMathMax.h"
#include "core/css/cssom/CSSMathMin.h"
#include "core/css/cssom/CSSMathNegate.h"
#include "core/css/cssom/CSSMathProduct.h"
#include "core/css/cssom/CSSMathSum.h"
#include "core/css/cssom/CSSUnitValue.h"
#include "core/css/parser/CSSParserTokenStream.h"
#include "core/css/parser/CSSTokenizer.h"
#include <numeric>
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());
CSSUnitValue* first_unit_value = ToCSSUnitValueOrNull(values[0]);
if (!first_unit_value)
return nullptr;
double final_value = first_unit_value->value();
for (size_t i = 1; i < values.size(); i++) {
CSSUnitValue* unit_value = ToCSSUnitValueOrNull(values[i]);
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 (size_t i = 0; i < values.size(); i++) {
CSSUnitValue* unit_value = ToCSSUnitValueOrNull(values[i]);
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);
}
CalcOperator CanonicalOperator(CalcOperator op) {
if (op == kCalcAdd || op == kCalcSubtract)
return kCalcAdd;
return kCalcMultiply;
}
bool CanCombineNodes(const CSSCalcExpressionNode& root,
const CSSCalcExpressionNode& node) {
DCHECK_EQ(root.GetType(), CSSCalcExpressionNode::kCssCalcBinaryOperation);
if (node.GetType() == CSSCalcExpressionNode::kCssCalcPrimitiveValue)
return false;
return !node.IsNestedCalc() && CanonicalOperator(root.OperatorType()) ==
CanonicalOperator(node.OperatorType());
}
CSSNumericValue* NegateOrInvertIfRequired(CalcOperator parent_op,
CSSNumericValue* value) {
DCHECK(value);
if (parent_op == kCalcSubtract)
return CSSMathNegate::Create(value);
if (parent_op == kCalcDivide)
return CSSMathInvert::Create(value);
return value;
}
CSSNumericValue* CalcToNumericValue(const CSSCalcExpressionNode& root) {
if (root.GetType() == CSSCalcExpressionNode::kCssCalcPrimitiveValue) {
const CSSPrimitiveValue::UnitType unit = root.TypeWithCalcResolved();
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));
}
// When the node is a binary operator, we return either a CSSMathSum or a
// CSSMathProduct.
DCHECK_EQ(root.GetType(), CSSCalcExpressionNode::kCssCalcBinaryOperation);
CSSNumericValueVector values;
// 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 CSSCalcExpressionNode* cur_node = &root;
do {
DCHECK(cur_node->LeftExpressionNode());
DCHECK(cur_node->RightExpressionNode());
const auto& value = CalcToNumericValue(*cur_node->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(cur_node->OperatorType(), value));
cur_node = cur_node->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());
if (root.OperatorType() == kCalcAdd || root.OperatorType() == kCalcSubtract)
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 (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);
auto range = stream.ConsumeUntilPeekedTypeIs<>();
if (!stream.AtEnd()) {
exception_state.ThrowDOMException(kSyntaxError, "Invalid math expression");
return nullptr;
}
switch (range.Peek().GetType()) {
case kNumberToken:
case kPercentageToken:
case kDimensionToken: {
const auto token = range.Consume();
if (!range.AtEnd())
break;
return CSSUnitValue::Create(token.NumericValue(), token.GetUnitType());
}
case kFunctionToken:
if (range.Peek().FunctionId() == CSSValueCalc ||
range.Peek().FunctionId() == CSSValueWebkitCalc) {
CSSCalcValue* calc_value = CSSCalcValue::Create(range, kValueRangeAll);
if (!calc_value)
break;
DCHECK(calc_value->ExpressionNode());
return CalcToNumericValue(*calc_value->ExpressionNode());
}
default:
break;
}
exception_state.ThrowDOMException(kSyntaxError, "Invalid math expression");
return nullptr;
}
CSSNumericValue* CSSNumericValue::FromCSSValue(const CSSPrimitiveValue& value) {
if (value.IsCalculated())
return CalcToNumericValue(*value.CssCalcValue()->ExpressionNode());
return CSSUnitValue::FromCSSValue(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(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(kSyntaxError,
"Invalid unit for conversion");
return nullptr;
}
}
const WTF::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::CodePointCompareLessThan(ToCSSUnitValue(a)->unit(),
ToCSSUnitValue(b)->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 = ToCSSUnitValue(*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;
}
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);
std::transform(values.begin(), values.end(), values.begin(),
[](CSSNumericValue* v) { return v->Invert(); });
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); });
}
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