From 3125a37a2b2465f5f10255430f4812ca777ac024 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 3 Feb 2025 14:31:29 -0800 Subject: [PATCH] [skip ci] CSSDataTypeParser consume() function (#48986) Summary: This adds a new `consume()` function to data type parsers which passes a raw parser. This can be used for types which are compounds of other data types, where we may want to accept more than the first token. This will be used for shadow parsing, but also fixes a hypothetical future bug with ratios. E.g. `calc(foo) / calc(bar)` may be a valid ratio, not starting with a token. We instead just want to try to parse a number data type from the stream. The form of parsing a preserved token + rest is removed, with the assumption that anything parsing more than a single token should use compound parsing. Changelog: [Internal] Reviewed By: lenaic Differential Revision: D68735370 --- .../react/renderer/css/CSSDataType.h | 24 ++-- .../ReactCommon/react/renderer/css/CSSRatio.h | 17 +-- .../react/renderer/css/CSSSyntaxParser.h | 108 +++++++++--------- .../react/renderer/css/CSSValueParser.h | 39 +++++-- .../renderer/css/tests/CSSValueParserTest.cpp | 66 +++++++++++ 5 files changed, 172 insertions(+), 82 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/css/tests/CSSValueParserTest.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSDataType.h b/packages/react-native/ReactCommon/react/renderer/css/CSSDataType.h index 3147bb39af86b0..ac533422ee1615 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSDataType.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSDataType.h @@ -48,29 +48,27 @@ concept CSSSimpleBlockSink = * concrete representation. */ template -concept CSSPreservedTokenSink = - requires(const CSSPreservedToken& token, CSSSyntaxParser& parser) { - { - T::consumePreservedToken(token, parser) - } -> std::convertible_to; - }; +concept CSSPreservedTokenSink = requires(const CSSPreservedToken& token) { + { T::consumePreservedToken(token) } -> std::convertible_to; +}; /** - * Accepts a CSS preserved token and may parse it into a concrete - * representation. + * Accepts a raw syntax parser, to be able to parse compounded values */ template -concept CSSSimplePreservedTokenSink = requires(const CSSPreservedToken& token) { - { T::consumePreservedToken(token) } -> std::convertible_to; +concept CSSParserSink = requires(CSSSyntaxParser& parser) { + { T::consume(parser) } -> std::convertible_to; }; /** * Represents a valid specialization of CSSDataTypeParser */ template -concept CSSValidDataTypeParser = CSSFunctionBlockSink || - CSSSimpleBlockSink || CSSPreservedTokenSink || - CSSSimplePreservedTokenSink; +concept CSSValidDataTypeParser = + ((CSSFunctionBlockSink || CSSSimpleBlockSink || + CSSPreservedTokenSink) && + !CSSParserSink) || + CSSParserSink; /** * Concrete representation for a CSS data type diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSRatio.h b/packages/react-native/ReactCommon/react/renderer/css/CSSRatio.h index 92111bbcc507f0..9adb77a8cd9216 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSRatio.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSRatio.h @@ -39,23 +39,26 @@ struct CSSRatio { template <> struct CSSDataTypeParser { - static constexpr auto consumePreservedToken( - const CSSPreservedToken& token, - CSSSyntaxParser& parser) -> std::optional { + static constexpr auto consume(CSSSyntaxParser& parser) + -> std::optional { // = [ / ]? // https://www.w3.org/TR/css-values-4/#ratio - if (token.numericValue() >= 0) { - float numerator = token.numericValue(); + auto numerator = parseNextCSSValue(parser); + if (!std::holds_alternative(numerator)) { + return {}; + } + auto numeratorValue = std::get(numerator).value; + if (numeratorValue >= 0) { auto denominator = peekNextCSSValue(parser, CSSDelimiter::Solidus); if (std::holds_alternative(denominator) && std::get(denominator).value >= 0) { parseNextCSSValue(parser, CSSDelimiter::Solidus); - return CSSRatio{numerator, std::get(denominator).value}; + return CSSRatio{numeratorValue, std::get(denominator).value}; } - return CSSRatio{numerator, 1.0f}; + return CSSRatio{numeratorValue, 1.0f}; } return {}; diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h b/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h index 67733c7957c295..9b84a75461123a 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h @@ -184,6 +184,59 @@ class CSSSyntaxParser { } } + /** + * Consume a delimiter, returning false if a required delimiter is not found. + */ + constexpr bool consumeDelimiter(CSSDelimiter delimiter) { + if (delimiter == CSSDelimiter::None) { + return true; + } + + bool hasWhiteSpace = peek().type() == CSSTokenType::WhiteSpace; + consumeWhitespace(); + + switch (delimiter) { + case CSSDelimiter::Comma: + if (peek().type() == CSSTokenType::Comma) { + consumeToken(); + consumeWhitespace(); + return true; + } + return false; + case CSSDelimiter::Whitespace: + return hasWhiteSpace; + case CSSDelimiter::OptionalWhitespace: + return true; + case CSSDelimiter::CommaOrWhitespace: + if (peek().type() == CSSTokenType::Comma) { + consumeToken(); + consumeWhitespace(); + return true; + } + return hasWhiteSpace; + case CSSDelimiter::Solidus: + if (peek().type() == CSSTokenType::Delim && + peek().stringValue() == "/") { + consumeToken(); + consumeWhitespace(); + return true; + } + return false; + case CSSDelimiter::SolidusOrWhitespace: + if (peek().type() == CSSTokenType::Delim && + peek().stringValue() == "/") { + consumeToken(); + consumeWhitespace(); + return true; + } + return hasWhiteSpace; + case CSSDelimiter::None: + return true; + } + + return false; + } + private: constexpr CSSSyntaxParser(CSSSyntaxParser& parser, CSSTokenType terminator) : tokenizer_{parser.tokenizer_}, @@ -220,7 +273,7 @@ struct CSSComponentValueVisitorDispatcher { CSSDelimiter delimiter, const VisitorsT&... visitors) { auto originalParser = parser; - if (!consumeDelimiter(delimiter)) { + if (!parser.consumeDelimiter(delimiter)) { parser = originalParser; return {}; } @@ -265,59 +318,6 @@ struct CSSComponentValueVisitorDispatcher { return ReturnT{}; } - /** - * Consume a delimiter, returning false if a required delimiter is not found. - */ - constexpr bool consumeDelimiter(CSSDelimiter delimiter) { - if (delimiter == CSSDelimiter::None) { - return true; - } - - bool hasWhiteSpace = parser.peek().type() == CSSTokenType::WhiteSpace; - parser.consumeWhitespace(); - - switch (delimiter) { - case CSSDelimiter::Comma: - if (parser.peek().type() == CSSTokenType::Comma) { - parser.consumeToken(); - parser.consumeWhitespace(); - return true; - } - return false; - case CSSDelimiter::Whitespace: - return hasWhiteSpace; - case CSSDelimiter::OptionalWhitespace: - return true; - case CSSDelimiter::CommaOrWhitespace: - if (parser.peek().type() == CSSTokenType::Comma) { - parser.consumeToken(); - parser.consumeWhitespace(); - return true; - } - return hasWhiteSpace; - case CSSDelimiter::Solidus: - if (parser.peek().type() == CSSTokenType::Delim && - parser.peek().stringValue() == "/") { - parser.consumeToken(); - parser.consumeWhitespace(); - return true; - } - return false; - case CSSDelimiter::SolidusOrWhitespace: - if (parser.peek().type() == CSSTokenType::Delim && - parser.peek().stringValue() == "/") { - parser.consumeToken(); - parser.consumeWhitespace(); - return true; - } - return hasWhiteSpace; - case CSSDelimiter::None: - return true; - } - - return false; - } - constexpr std::optional visitFunction( const CSSComponentValueVisitor auto& visitor, const CSSComponentValueVisitor auto&... rest) { diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSValueParser.h b/packages/react-native/ReactCommon/react/renderer/css/CSSValueParser.h index 7ce6935e7c690d..bfd9cd65566b44 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSValueParser.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSValueParser.h @@ -35,6 +35,14 @@ class CSSValueParser { CSSDelimiter delimeter = CSSDelimiter::None) { using ReturnT = std::variant; + auto consumedValue = + tryConsumeParser...>( + delimeter); + + if (!std::holds_alternative(consumedValue)) { + return consumedValue; + } + return parser_.consumeComponentValue( delimeter, [&](const CSSPreservedToken& token) { @@ -75,14 +83,6 @@ class CSSValueParser { CSSValidDataTypeParser... RestParserT> constexpr ReturnT tryConsumePreservedToken(const CSSPreservedToken& token) { if constexpr (CSSPreservedTokenSink) { - auto currentParser = parser_; - if (auto ret = ParserT::consumePreservedToken(token, parser_)) { - return *ret; - } - parser_ = currentParser; - } - - if constexpr (CSSSimplePreservedTokenSink) { if (auto ret = ParserT::consumePreservedToken(token)) { return *ret; } @@ -141,6 +141,29 @@ class CSSValueParser { return tryConsumeFunctionBlock(func, blockParser); } + template + constexpr ReturnT tryConsumeParser(CSSDelimiter /*delimeter*/) { + return {}; + } + + template < + typename ReturnT, + CSSValidDataTypeParser ParserT, + CSSValidDataTypeParser... RestParserT> + constexpr ReturnT tryConsumeParser(CSSDelimiter delimeter) { + if constexpr (CSSParserSink) { + auto originalParser = parser_; + if (parser_.consumeDelimiter(delimeter)) { + if (auto ret = ParserT::consume(parser_)) { + return *ret; + } + } + parser_ = originalParser; + } + + return tryConsumeParser(delimeter); + } + CSSSyntaxParser& parser_; }; diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSValueParserTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSValueParserTest.cpp new file mode 100644 index 00000000000000..b2384abbb94ca5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSValueParserTest.cpp @@ -0,0 +1,66 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +namespace facebook::react { + +struct ConsumeDataType { + float number{}; + + constexpr bool operator==(const ConsumeDataType& other) const = default; +}; + +template <> +struct CSSDataTypeParser { + constexpr static std::optional consume( + CSSSyntaxParser& parser) { + auto val = parseNextCSSValue(parser); + if (std::holds_alternative(val)) { + return ConsumeDataType{std::get(val).value}; + } + + return {}; + } +}; + +static_assert(CSSDataType); + +TEST(CSSValueParser, consume_multiple_with_delimeter) { + CSSSyntaxParser parser{"1 2, 3, 4 / 5"}; + + auto next = parseNextCSSValue(parser); + EXPECT_TRUE(std::holds_alternative(next)); + EXPECT_EQ(std::get(next).number, 1); + + next = parseNextCSSValue(parser, CSSDelimiter::None); + EXPECT_FALSE(std::holds_alternative(next)); + + next = parseNextCSSValue(parser, CSSDelimiter::Whitespace); + EXPECT_TRUE(std::holds_alternative(next)); + EXPECT_EQ(std::get(next).number, 2); + + next = parseNextCSSValue(parser, CSSDelimiter::Comma); + EXPECT_TRUE(std::holds_alternative(next)); + EXPECT_EQ(std::get(next).number, 3); + + next = parseNextCSSValue(parser, CSSDelimiter::Comma); + EXPECT_TRUE(std::holds_alternative(next)); + EXPECT_EQ(std::get(next).number, 4); + + next = parseNextCSSValue(parser, CSSDelimiter::Solidus); + EXPECT_TRUE(std::holds_alternative(next)); + EXPECT_EQ(std::get(next).number, 5); + + next = parseNextCSSValue(parser); + EXPECT_FALSE(std::holds_alternative(next)); +} + +} // namespace facebook::react