diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSMathConstant.h b/packages/react-native/ReactCommon/react/renderer/css/CSSMathConstant.h new file mode 100644 index 000000000000..3c64aaef413a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSMathConstant.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook::react { + +/** + * Numeric keyword constants usable within CSS math functions. + * https://www.w3.org/TR/css-values-4/#calc-constants + */ +enum class CSSMathConstant { + E, + Pi, + Infinity, + NegativeInfinity, + NaN, +}; + +constexpr auto parseCSSMathConstant(std::string_view unit) -> std::optional +{ + switch (fnv1aLowercase(unit)) { + case fnv1a("e"): + return CSSMathConstant::E; + case fnv1a("pi"): + return CSSMathConstant::Pi; + case fnv1a("infinity"): + return CSSMathConstant::Infinity; + case fnv1a("-infinity"): + return CSSMathConstant::NegativeInfinity; + case fnv1a("nan"): + return CSSMathConstant::NaN; + default: + return std::nullopt; + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSMathOperator.h b/packages/react-native/ReactCommon/react/renderer/css/CSSMathOperator.h new file mode 100644 index 000000000000..053e42ac1fda --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSMathOperator.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +/** + * Arithmetic operators used in CSS math functions. + * https://www.w3.org/TR/css-values-4/#calc-syntax + */ +enum class CSSMathOperator : char { + Add = '+', + Subtract = '-', + Multiply = '*', + Divide = '/', +}; + +constexpr auto parseCSSMathOperator(char c) -> std::optional +{ + for (auto op : {CSSMathOperator::Add, CSSMathOperator::Subtract, + CSSMathOperator::Multiply, CSSMathOperator::Divide}) { + if (c == to_underlying(op)) { + return op; + } + } + return std::nullopt; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSToken.h b/packages/react-native/ReactCommon/react/renderer/css/CSSToken.h index 4194ea179924..a7f2c612f58d 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSToken.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSToken.h @@ -7,8 +7,12 @@ #pragma once +#include #include +#include +#include + namespace facebook::react { /** @@ -76,6 +80,33 @@ class CSSToken { return unit_; } + /** + * If this token is a representing a math operator (+, -, *, + * /), return the corresponding CSSMathOperator. + * https://www.w3.org/TR/css-values-4/#calc-syntax + */ + constexpr std::optional mathOperator() const + { + if (type_ != CSSTokenType::Delim || stringValue_.size() != 1) { + return std::nullopt; + } + return parseCSSMathOperator(stringValue_[0]); + } + + /** + * If this token is an representing a CSS math constant + * (e, pi, infinity, -infinity, NaN), return the corresponding + * CSSMathConstant + * https://www.w3.org/TR/css-values-4/#calc-constants + */ + constexpr std::optional mathConstant() const + { + if (type_ != CSSTokenType::Ident) { + return std::nullopt; + } + return parseCSSMathConstant(stringValue_); + } + constexpr bool operator==(const CSSToken &other) const = default; private: diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSTokenizer.h b/packages/react-native/ReactCommon/react/renderer/css/CSSTokenizer.h index 17e3e6ccd708..379ab13f7742 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSTokenizer.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSTokenizer.h @@ -54,13 +54,20 @@ class CSSTokenizer { case ',': return consumeCharacter(CSSTokenType::Comma); case '+': - case '-': case '.': if (wouldStartNumber()) { return consumeNumeric(); } else { return consumeDelim(); } + case '-': + if (wouldStartNumber()) { + return consumeNumeric(); + } else if (wouldStartIdentSequence()) { + return consumeIdentlikeToken(); + } else { + return consumeDelim(); + } case '#': if (isIdent(peek(1))) { return consumeHash(); @@ -140,6 +147,16 @@ class CSSTokenizer { return false; } + constexpr bool wouldStartIdentSequence() const + { + // https://www.w3.org/TR/css-syntax-3/#would-start-an-identifier + if (peek() == '-') { + return isIdentStart(peek(1)) || peek(1) == '-'; + } + + return isIdentStart(peek()); + } + constexpr CSSToken consumeNumber() { // https://www.w3.org/TR/css-syntax-3/#consume-number diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTokenizerTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTokenizerTest.cpp index 33b67e14b32a..e1c1b017b7d8 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTokenizerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSTokenizerTest.cpp @@ -58,6 +58,48 @@ TEST(CSSTokenizer, ident_values) { CSSToken{CSSTokenType::WhiteSpace}, CSSToken{CSSTokenType::Ident, "left"}, CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "-infinity", + CSSToken{CSSTokenType::Ident, "-infinity"}, + CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "--custom-ident", + CSSToken{CSSTokenType::Ident, "--custom-ident"}, + CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "e pi infinity NaN", + CSSToken{CSSTokenType::Ident, "e"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Ident, "pi"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Ident, "infinity"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Ident, "NaN"}, + CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "5 - 3", + CSSToken{CSSTokenType::Number, 5.0f}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Delim, "-"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Number, 3.0f}, + CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "5 -3", + CSSToken{CSSTokenType::Number, 5.0f}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Number, -3.0f}, + CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "-calc(", + CSSToken{CSSTokenType::Function, "-calc"}, + CSSToken{CSSTokenType::EndOfFile}); } TEST(CSSTokenizer, number_values) { @@ -220,6 +262,17 @@ TEST(CSSTokenizer, invalid_values) { CSSToken{CSSTokenType::OpenParen}, CSSToken{CSSTokenType::Delim, "%"}, CSSToken{CSSTokenType::EndOfFile}); + + EXPECT_TOKENS( + "+ - * /", + CSSToken{CSSTokenType::Delim, "+"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Delim, "-"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Delim, "*"}, + CSSToken{CSSTokenType::WhiteSpace}, + CSSToken{CSSTokenType::Delim, "/"}, + CSSToken{CSSTokenType::EndOfFile}); } TEST(CSSTokenizer, hash_values) { @@ -239,4 +292,23 @@ TEST(CSSTokenizer, hash_values) { CSSToken{CSSTokenType::Delim, "*"}, CSSToken{CSSTokenType::EndOfFile}); } +TEST(CSSTokenizer, mathOperator) { + EXPECT_EQ(CSSTokenizer{"+"}.next().mathOperator(), CSSMathOperator::Add); + EXPECT_EQ(CSSTokenizer{"-"}.next().mathOperator(), CSSMathOperator::Subtract); + EXPECT_EQ(CSSTokenizer{"*"}.next().mathOperator(), CSSMathOperator::Multiply); + EXPECT_EQ(CSSTokenizer{"/"}.next().mathOperator(), CSSMathOperator::Divide); + EXPECT_EQ(CSSTokenizer{"%"}.next().mathOperator(), std::nullopt); +} +TEST(CSSTokenizer, math_constants) { + EXPECT_EQ(CSSTokenizer{"pi"}.next().mathConstant(), CSSMathConstant::Pi); + EXPECT_EQ(CSSTokenizer{"e"}.next().mathConstant(), CSSMathConstant::E); + EXPECT_EQ( + CSSTokenizer{"infinity"}.next().mathConstant(), + CSSMathConstant::Infinity); + EXPECT_EQ( + CSSTokenizer{"-infinity"}.next().mathConstant(), + CSSMathConstant::NegativeInfinity); + EXPECT_EQ(CSSTokenizer{"NaN"}.next().mathConstant(), CSSMathConstant::NaN); + EXPECT_EQ(CSSTokenizer{"abc"}.next().mathConstant(), std::nullopt); +} } // namespace facebook::react