From 6879e094cfd2c9ee14d339b4b970015ec32237b7 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 4 Feb 2025 11:15:20 -0800 Subject: [PATCH 1/3] [skip ci] Do not consume component value on visitor failure (#48985) Summary: This reverts some of the behavior I added in D68357624, since peeking a component value is non-obviously more expensive than manually copying the parser, and needing to peek will be a pain for flat lists of values (like for box-shadow). Changelog: [internal] Reviewed By: lenaic Differential Revision: D68733518 --- .../react/renderer/css/CSSSyntaxParser.h | 123 ++++++------------ .../css/tests/CSSSyntaxParserTest.cpp | 117 +++++------------ 2 files changed, 73 insertions(+), 167 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h b/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h index 22c96834b9f971..67733c7957c295 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h @@ -35,6 +35,13 @@ struct CSSSimpleBlock { CSSTokenType openBracketType{}; }; +/** + * Describes a valid return type for a CSSSyntaxParser visitor + */ +template +concept CSSSyntaxVisitorReturn = + std::is_default_constructible_v && std::equality_comparable; + /** * A CSSFunctionVisitor is called to start parsing a function component value. * At this point, the Parser is positioned at the start of the function @@ -42,7 +49,7 @@ struct CSSSimpleBlock { * of the function block. */ template -concept CSSFunctionVisitor = +concept CSSFunctionVisitor = CSSSyntaxVisitorReturn && requires(T visitor, CSSFunctionBlock func, CSSSyntaxParser& blockParser) { { visitor(func, blockParser) } -> std::convertible_to; }; @@ -52,7 +59,7 @@ concept CSSFunctionVisitor = * component value. */ template -concept CSSPreservedTokenVisitor = +concept CSSPreservedTokenVisitor = CSSSyntaxVisitorReturn && requires(T visitor, CSSPreservedToken token) { { visitor(token) } -> std::convertible_to; }; @@ -63,7 +70,7 @@ concept CSSPreservedTokenVisitor = * of the block. */ template -concept CSSSimpleBlockVisitor = +concept CSSSimpleBlockVisitor = CSSSyntaxVisitorReturn && requires(T visitor, CSSSimpleBlock block, CSSSyntaxParser& blockParser) { { visitor(block, blockParser) } -> std::convertible_to; }; @@ -72,16 +79,17 @@ concept CSSSimpleBlockVisitor = * Any visitor for a component value. */ template -concept CSSComponentValueVisitor = CSSFunctionVisitor || - CSSPreservedTokenVisitor || CSSSimpleBlockVisitor; +concept CSSComponentValueVisitor = CSSSyntaxVisitorReturn && + (CSSFunctionVisitor || CSSPreservedTokenVisitor || + CSSSimpleBlockVisitor); /** * Represents a variadic set of CSSComponentValueVisitor with no more than one * of a specific type of visitor. */ template -concept CSSUniqueComponentValueVisitors = - (CSSComponentValueVisitor && ... && true) && +concept CSSUniqueComponentValueVisitors = CSSSyntaxVisitorReturn && + (CSSComponentValueVisitor && ...) && ((CSSFunctionVisitor ? 1 : 0) + ... + 0) <= 1 && ((CSSPreservedTokenVisitor ? 1 : 0) + ... + 0) <= 1 && ((CSSSimpleBlockVisitor ? 1 : 0) + ... + 0) <= 1; @@ -106,7 +114,9 @@ enum class CSSDelimiter { * https://www.w3.org/TR/css-syntax-3/#component-value */ class CSSSyntaxParser { - template ... VisitorsT> + template < + CSSSyntaxVisitorReturn ReturnT, + CSSComponentValueVisitor... VisitorsT> friend struct CSSComponentValueVisitorDispatcher; public: @@ -130,6 +140,11 @@ class CSSSyntaxParser { * higher-level data structure, and continue parsing within its scope using * the same underlying CSSSyntaxParser. * + * The state of the parser is reset if a visitor returns a default-constructed + * value for the given return type, even if it previously advanced the parser. + * If no visitor returns a non-default-constructed value, the component value + * will not be consumed. + * * https://www.w3.org/TR/css-syntax-3/#consume-component-value * * @param caller-specified return type of visitors. This type will @@ -142,47 +157,16 @@ class CSSSyntaxParser { * @returns the visitor returned value, or a default constructed value if no * visitor was matched, or a syntax error occurred. */ - template + template constexpr ReturnT consumeComponentValue( CSSDelimiter delimiter, const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors); - template + template constexpr ReturnT consumeComponentValue( const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors); - - /** - * Peek at the next component value without consuming it. The component value - * is provided to a passed in "visitor", typically a lambda which accepts the - * component value in a new scope. The visitor may read this component - * parameter into a higher-level data structure, and continue parsing within - * its scope using the same underlying CSSSyntaxParser. - * - * https://www.w3.org/TR/css-syntax-3/#consume-component-value - * - * @param caller-specified return type of visitors. This type will - * be set to its default constructed state if consuming a component value with - * no matching visitors, or syntax error - * @param visitors A unique list of CSSComponentValueVisitor to be called on a - * match - * @param delimiter The expected delimeter to occur before the next component - * value - * @returns the visitor returned value, or a default constructed value if no - * visitor was matched, or a syntax error occurred. - */ - template - constexpr ReturnT peekComponentValue( - CSSDelimiter delimiter, - const CSSComponentValueVisitor auto&... visitors) - requires(CSSUniqueComponentValueVisitors); - - template - constexpr ReturnT peekComponentValue( - const CSSComponentValueVisitor auto&... visitors) - requires(CSSUniqueComponentValueVisitors); - /** * The parser is considered finished when there are no more remaining tokens * to be processed @@ -226,7 +210,9 @@ class CSSSyntaxParser { CSSTokenType terminator_{CSSTokenType::EndOfFile}; }; -template ... VisitorsT> +template < + CSSSyntaxVisitorReturn ReturnT, + CSSComponentValueVisitor... VisitorsT> struct CSSComponentValueVisitorDispatcher { CSSSyntaxParser& parser; @@ -275,6 +261,7 @@ struct CSSComponentValueVisitorDispatcher { break; } + parser = originalParser; return ReturnT{}; } @@ -331,15 +318,6 @@ struct CSSComponentValueVisitorDispatcher { return false; } - constexpr ReturnT peekComponentValue( - CSSDelimiter delimiter, - const VisitorsT&... visitors) { - auto originalParser = parser; - auto ret = consumeComponentValue(delimiter, visitors...); - parser = originalParser; - return ret; - } - constexpr std::optional visitFunction( const CSSComponentValueVisitor auto& visitor, const CSSComponentValueVisitor auto&... rest) { @@ -357,7 +335,8 @@ struct CSSComponentValueVisitorDispatcher { auto functionValue = visitor({name}, blockParser); parser.advanceToBlockParser(blockParser); parser.consumeWhitespace(); - if (parser.peek().type() == CSSTokenType::CloseParen) { + if (parser.peek().type() == CSSTokenType::CloseParen && + functionValue != ReturnT{}) { parser.consumeToken(); return functionValue; } @@ -369,11 +348,6 @@ struct CSSComponentValueVisitorDispatcher { } constexpr std::optional visitFunction() { - while (parser.peek().type() != CSSTokenType::CloseParen) { - parser.consumeToken(); - } - parser.consumeToken(); - return {}; } @@ -388,7 +362,7 @@ struct CSSComponentValueVisitorDispatcher { auto blockValue = visitor({openBracketType}, blockParser); parser.advanceToBlockParser(blockParser); parser.consumeWhitespace(); - if (parser.peek().type() == endToken) { + if (parser.peek().type() == endToken && blockValue != ReturnT{}) { parser.consumeToken(); return blockValue; } @@ -399,10 +373,6 @@ struct CSSComponentValueVisitorDispatcher { } constexpr std::optional visitSimpleBlock(CSSTokenType endToken) { - while (parser.peek().type() != endToken) { - parser.consumeToken(); - } - parser.consumeToken(); return {}; } @@ -410,18 +380,20 @@ struct CSSComponentValueVisitorDispatcher { const CSSComponentValueVisitor auto& visitor, const CSSComponentValueVisitor auto&... rest) { if constexpr (CSSPreservedTokenVisitor) { - return visitor(parser.consumeToken()); + auto ret = visitor(parser.consumeToken()); + if (ret != ReturnT{}) { + return ret; + } } return visitPreservedToken(rest...); } constexpr std::optional visitPreservedToken() { - parser.consumeToken(); return {}; } }; -template +template constexpr ReturnT CSSSyntaxParser::consumeComponentValue( CSSDelimiter delimiter, const CSSComponentValueVisitor auto&... visitors) @@ -432,7 +404,7 @@ constexpr ReturnT CSSSyntaxParser::consumeComponentValue( .consumeComponentValue(delimiter, visitors...); } -template +template constexpr ReturnT CSSSyntaxParser::consumeComponentValue( const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors) @@ -440,23 +412,4 @@ constexpr ReturnT CSSSyntaxParser::consumeComponentValue( return consumeComponentValue(CSSDelimiter::None, visitors...); } -template -constexpr ReturnT CSSSyntaxParser::peekComponentValue( - CSSDelimiter delimiter, - const CSSComponentValueVisitor auto&... visitors) - requires(CSSUniqueComponentValueVisitors) -{ - return CSSComponentValueVisitorDispatcher{ - *this} - .peekComponentValue(delimiter, visitors...); -} - -template -constexpr ReturnT CSSSyntaxParser::peekComponentValue( - const CSSComponentValueVisitor auto&... visitors) - requires(CSSUniqueComponentValueVisitors) -{ - return peekComponentValue(CSSDelimiter::None, visitors...); -} - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSSyntaxParserTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSSyntaxParserTest.cpp index 86336b104de1e3..494d4fc464ea08 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSSyntaxParserTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSSyntaxParserTest.cpp @@ -382,88 +382,6 @@ TEST(CSSSyntaxParser, unconsumed_simple_block_args) { EXPECT_EQ(funcValue, std::nullopt); } -TEST(CSSSyntaxParser, peek_does_not_consume) { - CSSSyntaxParser parser{"foo()"}; - auto funcValue = parser.peekComponentValue>( - [&](const CSSFunctionBlock& function, CSSSyntaxParser& /*blockParser*/) { - EXPECT_EQ(function.name, "foo"); - return function.name; - }); - - EXPECT_EQ(funcValue, "foo"); - - auto funcValue2 = parser.peekComponentValue>( - [&](const CSSFunctionBlock& function, CSSSyntaxParser& /*blockParser*/) { - EXPECT_EQ(function.name, "foo"); - return function.name; - }); - - EXPECT_EQ(funcValue2, "foo"); - - auto funcValue3 = - parser.consumeComponentValue>( - [&](const CSSFunctionBlock& function, - CSSSyntaxParser& /*blockParser*/) { - EXPECT_EQ(function.name, "foo"); - return function.name; - }); - - EXPECT_EQ(funcValue3, "foo"); - - auto funcValue4 = parser.peekComponentValue>( - [&](const CSSFunctionBlock& function, CSSSyntaxParser& /*blockParser*/) { - EXPECT_EQ(function.name, "foo"); - return function.name; - }); - - EXPECT_EQ(funcValue4, std::nullopt); -} - -TEST(CSSSyntaxParser, preserved_token_without_visitor_consumed) { - CSSSyntaxParser parser{"foo bar"}; - - parser.consumeComponentValue(); - - auto identValue = parser.consumeComponentValue( - CSSDelimiter::Whitespace, [](const CSSPreservedToken& token) { - EXPECT_EQ(token.type(), CSSTokenType::Ident); - EXPECT_EQ(token.stringValue(), "bar"); - return token.stringValue(); - }); - - EXPECT_EQ(identValue, "bar"); -} - -TEST(CSSSyntaxParser, function_without_visitor_consumed) { - CSSSyntaxParser parser{"foo(a, b, c) bar"}; - - parser.consumeComponentValue(); - - auto identValue = parser.consumeComponentValue( - CSSDelimiter::Whitespace, [](const CSSPreservedToken& token) { - EXPECT_EQ(token.type(), CSSTokenType::Ident); - EXPECT_EQ(token.stringValue(), "bar"); - return token.stringValue(); - }); - - EXPECT_EQ(identValue, "bar"); -} - -TEST(CSSSyntaxParser, simple_block_without_visitor_consumed) { - CSSSyntaxParser parser{"{a foo(abc)} bar"}; - - parser.consumeComponentValue(); - - auto identValue = parser.consumeComponentValue( - CSSDelimiter::Whitespace, [](const CSSPreservedToken& token) { - EXPECT_EQ(token.type(), CSSTokenType::Ident); - EXPECT_EQ(token.stringValue(), "bar"); - return token.stringValue(); - }); - - EXPECT_EQ(identValue, "bar"); -} - TEST(CSSSyntaxParser, solidus_delimiter) { CSSSyntaxParser parser{"foo / bar"}; @@ -606,4 +524,39 @@ TEST(CSSSyntaxParser, delimeter_not_consumed_when_no_component_value) { EXPECT_TRUE(hasComma); } +TEST(CSSSyntaxParser, component_value_not_consumed_on_visitor_failure) { + CSSSyntaxParser parser{"foo"}; + + bool visitor1Attempted = false; + bool visitor1Ret = + parser.consumeComponentValue([&](const CSSPreservedToken& token) { + EXPECT_EQ(token.stringValue(), "foo"); + visitor1Attempted = true; + return false; + }); + + EXPECT_TRUE(visitor1Attempted); + EXPECT_FALSE(visitor1Ret); + + bool visitor2Attempted = false; + parser.consumeComponentValue([&](const CSSPreservedToken& token) { + EXPECT_EQ(token.stringValue(), "foo"); + visitor2Attempted = true; + return true; + }); + + EXPECT_TRUE(visitor2Attempted); + EXPECT_TRUE(visitor2Attempted); + + bool visitor3Attempted = false; + bool visitor3Ret = + parser.consumeComponentValue([&](const CSSPreservedToken& token) { + visitor3Attempted = true; + return true; + }); + + EXPECT_FALSE(visitor3Attempted); + EXPECT_FALSE(visitor3Ret); +} + } // namespace facebook::react From 91e2001ae0e0d10a5b156c1b20e0c155d987d261 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 4 Feb 2025 11:15:20 -0800 Subject: [PATCH 2/3] [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 From 78bdbef193c27e89819f90dd6a8c4e11b9fd8677 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 4 Feb 2025 11:15:20 -0800 Subject: [PATCH 3/3] CSSCommaSeparatedList (#48987) Summary: Adds a data type parser for a variable number of values of a given single data type (at least 1). E.g. `CSSCommaSeparatedList` will represent the syntax of `#` (ie the value produced by box-shadow). Changelog: [internal] Reviewed By: lenaic Differential Revision: D68738165 --- .../renderer/css/CSSCommaSeparatedList.h | 45 +++++++++ .../css/tests/CSSCommaSeparatedListTest.cpp | 92 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSCommaSeparatedList.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/tests/CSSCommaSeparatedListTest.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSCommaSeparatedList.h b/packages/react-native/ReactCommon/react/renderer/css/CSSCommaSeparatedList.h new file mode 100644 index 00000000000000..ee640e65e9d3ef --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSCommaSeparatedList.h @@ -0,0 +1,45 @@ +/* + * 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 +#include + +namespace facebook::react { + +/** + * Represents a comma-separated repetition of a given single type. + * https://www.w3.org/TR/css-values-4/#mult-comma + */ +template +struct CSSCommaSeparatedList : public std::vector {}; + +template +struct CSSDataTypeParser> { + static inline auto consume(CSSSyntaxParser& parser) + -> std::optional> { + CSSCommaSeparatedList result; + for (auto nextValue = parseNextCSSValue(parser); + !std::holds_alternative(nextValue); + nextValue = + parseNextCSSValue(parser, CSSDelimiter::Comma)) { + result.push_back(std::move(std::get(nextValue))); + } + + if (result.empty()) { + return {}; + } + + return result; + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSCommaSeparatedListTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSCommaSeparatedListTest.cpp new file mode 100644 index 00000000000000..d96ef2f2073d7c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSCommaSeparatedListTest.cpp @@ -0,0 +1,92 @@ +/* + * 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 { + +TEST(CSSCommaSeparatedList, empty_values) { + auto emptyValue = parseCSSProperty>(""); + EXPECT_TRUE(std::holds_alternative(emptyValue)); + + auto whitespaceValue = + parseCSSProperty>(" "); + EXPECT_TRUE(std::holds_alternative(whitespaceValue)); + + auto commaValue = parseCSSProperty>(","); +} + +TEST(CSSCommaSeparatedList, single_value) { + auto simpleValue = parseCSSProperty>("20"); + EXPECT_TRUE( + std::holds_alternative>(simpleValue)); + EXPECT_EQ(std::get>(simpleValue).size(), 1); + EXPECT_EQ( + std::get>(simpleValue)[0].value, 20); + + auto whitespaceValue = + parseCSSProperty>(" 20 "); + EXPECT_TRUE(std::holds_alternative>( + whitespaceValue)); + EXPECT_EQ( + std::get>(whitespaceValue).size(), 1); + EXPECT_EQ( + std::get>(whitespaceValue)[0].value, 20); +} + +TEST(CSSCommaSeparatedList, wrong_type) { + auto simpleValue = parseCSSProperty>("20px"); + EXPECT_TRUE(std::holds_alternative(simpleValue)); +} + +TEST(CSSCommaSeparatedList, multiple_values) { + auto simpleValue = + parseCSSProperty>("20, 30, 40"); + EXPECT_TRUE( + std::holds_alternative>(simpleValue)); + EXPECT_EQ(std::get>(simpleValue).size(), 3); + EXPECT_EQ( + std::get>(simpleValue)[0].value, 20); + EXPECT_EQ( + std::get>(simpleValue)[1].value, 30); + EXPECT_EQ( + std::get>(simpleValue)[2].value, 40); + + auto whitespaceValue = + parseCSSProperty>(" 20 , 30 , 40 "); + EXPECT_TRUE(std::holds_alternative>( + whitespaceValue)); + EXPECT_EQ( + std::get>(whitespaceValue).size(), 3); + EXPECT_EQ( + std::get>(whitespaceValue)[0].value, 20); + EXPECT_EQ( + std::get>(whitespaceValue)[1].value, 30); + EXPECT_EQ( + std::get>(whitespaceValue)[2].value, 40); +} + +TEST(CSSCommaSeparatedList, extra_tokens) { + auto extraTokensValue = + parseCSSProperty>("20, 30, 40 50"); + EXPECT_TRUE(std::holds_alternative(extraTokensValue)); +} + +TEST(CSSCommaSeparatedList, extra_commas) { + auto prefixCommaValue = + parseCSSProperty>(",20"); + EXPECT_TRUE(std::holds_alternative(prefixCommaValue)); + + auto suffixCommaValue = + parseCSSProperty>("20,"); + EXPECT_TRUE(std::holds_alternative(suffixCommaValue)); +} + +} // namespace facebook::react