Skip to content

Commit

Permalink
Revise ComponentValue parsing model (#44561)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #44561

D57089275 introduced a layer to parse component values out of the token stream. I modeled this similar to the tokenizer, as a flat iterator of component values. Because function components can nest a variable number of child component values, this now looks like storing a fully resolved tree of tokens on the heap during parsing.

This diff changes the model, so that `CSSSyntaxParser::consumeComponentValue()` no longer returns a resolved CSS function value. Instead, users of the parser are expected to provide "visitors" which continue parsing, matched based on component value type pattern matched. Visitors can perform parsing specific to their context, and propagate values up the stack, based on their evaluation of the component value.

Removing the heap allocated list of tokens here also lets this core CSS parsing stack keep constexpr, so I added that back, though we need to keep expression trees for math expressions in uncommon cases, so the layer up probaly won't keep constexpr.

Changelog: [Internal]

Reviewed By: joevilches

Differential Revision: D57206706

fbshipit-source-id: 25db84d376ef18f6291e60ed953e29c4000a7a26
  • Loading branch information
NickGerleman authored and facebook-github-bot committed May 16, 2024
1 parent 0c7095f commit 258b481
Show file tree
Hide file tree
Showing 6 changed files with 657 additions and 279 deletions.
305 changes: 261 additions & 44 deletions packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,159 @@

#pragma once

#include <concepts>
#include <optional>
#include <variant>
#include <vector>

#include <react/renderer/css/CSSTokenizer.h>

namespace facebook::react {

/**
* CSSSyntaxParser allows parsing streams of CSS text into "component values",
* being either a preserved token, or a function.
* Describes context for a CSS function component value.
*/
struct CSSFunctionBlock {
std::string_view name{};
};

/**
* Describes a preserved token component value.
*/
using CSSPreservedToken = CSSToken;

/**
* Describes context for a CSS function component value.
*/
struct CSSSimpleBlock {
CSSTokenType openBracketType{};
};

/**
* A CSSFunctionVisitor is called to start parsing a function component value.
* At this point, the Parser is positioned at the start of the function
* component value list. It is expected that the visitor finishes before the end
* of the function block.
*/
template <typename T, typename ReturnT>
concept CSSFunctionVisitor = requires(T visitor, CSSFunctionBlock func) {
{ visitor(func) } -> std::convertible_to<ReturnT>;
};

/**
* A CSSPreservedTokenVisitor is called after parsing a preserved token
* component value.
*/
template <typename T, typename ReturnT>
concept CSSPreservedTokenVisitor =
requires(T visitor, CSSPreservedToken token) {
{ visitor(token) } -> std::convertible_to<ReturnT>;
};

/**
* A CSSSimpleBlockVisitor is called after parsing a simple block component
* value. It is expected that the visitor finishes before the end
* of the block.
*/
template <typename T, typename ReturnT>
concept CSSSimpleBlockVisitor = requires(T visitor, CSSSimpleBlock block) {
{ visitor(block) } -> std::convertible_to<ReturnT>;
};

/**
* Any visitor for a component value.
*/
template <typename T, typename ReturnT>
concept CSSComponentValueVisitor = CSSFunctionVisitor<T, ReturnT> ||
CSSPreservedTokenVisitor<T, ReturnT> || CSSSimpleBlockVisitor<T, ReturnT>;

/**
* Represents a variadic set of CSSComponentValueVisitor with no more than one
* of a specific type of visitor.
*/
template <typename ReturnT, typename... VisitorsT>
concept CSSUniqueComponentValueVisitors =
(CSSComponentValueVisitor<VisitorsT, ReturnT> && ...) &&
((CSSFunctionVisitor<VisitorsT, ReturnT> ? 1 : 0) + ... + 0) <= 1 &&
((CSSPreservedTokenVisitor<VisitorsT, ReturnT> ? 1 : 0) + ... + 0) <= 1 &&
((CSSSimpleBlockVisitor<VisitorsT, ReturnT> ? 1 : 0) + ... + 0) <= 1;

/**
* Describes the delimeter to expect before the next component value.
*/
enum class CSSComponentValueDelimiter {
Comma,
Whitespace,
None,
};

/**
* CSSSyntaxParser allows parsing streams of CSS text into "component
* values".
*
* https://www.w3.org/TR/css-syntax-3/#component-value
*/
class CSSSyntaxParser {
public:
struct Function;

using PreservedToken = CSSToken;
using ComponentValue = std::variant<std::monostate, PreservedToken, Function>;

struct Function {
std::string_view name{};
std::vector<ComponentValue> args{};
};
template <typename ReturnT, CSSComponentValueVisitor<ReturnT>... VisitorsT>
friend struct CSSComponentValueVisitorDispatcher;

public:
/**
* Construct the parser over the given string_view, which must stay alive for
* the duration of the CSSSyntaxParser.
*/
explicit constexpr CSSSyntaxParser(std::string_view css)
: tokenizer_{css}, currentToken_(tokenizer_.next()) {}

constexpr CSSSyntaxParser(const CSSSyntaxParser&) = default;
constexpr CSSSyntaxParser(CSSSyntaxParser&&) = default;

constexpr CSSSyntaxParser& operator=(const CSSSyntaxParser&) = default;
constexpr CSSSyntaxParser& operator=(CSSSyntaxParser&&) = default;

/**
* Directly consume the next component value
* Directly consume the next component value. 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 <ReturnT> 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 <typename ReturnT>
constexpr ReturnT consumeComponentValue(
CSSComponentValueDelimiter delimiter,
const CSSComponentValueVisitor<ReturnT> auto&... visitors)
requires(CSSUniqueComponentValueVisitors<ReturnT, decltype(visitors)...>);

template <typename ReturnT>
constexpr ReturnT consumeComponentValue(
const CSSComponentValueVisitor<ReturnT> auto&... visitors)
requires(CSSUniqueComponentValueVisitors<ReturnT, decltype(visitors)...>);

/**
* The parser is considered finished when there are no more remaining tokens
* to be processed
*/
inline ComponentValue consumeComponentValue() {
if (peek().type() == CSSTokenType::Function) {
auto function = consumeFunction();
return function.has_value() ? ComponentValue{std::move(*function)}
: ComponentValue{};
} else {
return consumeToken();
constexpr bool isFinished() const {
return currentToken_.type() == CSSTokenType::EndOfFile;
}

/**
* Consume any whitespace tokens.
*/
constexpr void consumeWhitespace() {
if (currentToken_.type() == CSSTokenType::WhiteSpace) {
currentToken_ = tokenizer_.next();
}
}

Expand All @@ -66,40 +174,149 @@ class CSSSyntaxParser {
return prevToken;
}

inline std::optional<Function> consumeFunction() {
// https://www.w3.org/TR/css-syntax-3/#consume-a-function
Function function{.name = consumeToken().stringValue()};
CSSTokenizer tokenizer_;
CSSToken currentToken_;
};

while (true) {
auto nextValue = consumeComponentValue();
if (std::holds_alternative<std::monostate>(nextValue)) {
return {};
}
template <typename ReturnT, CSSComponentValueVisitor<ReturnT>... VisitorsT>
struct CSSComponentValueVisitorDispatcher {
CSSSyntaxParser& parser;

if (auto token = std::get_if<CSSToken>(&nextValue)) {
if (token->type() == CSSTokenType::CloseParen) {
return function;
constexpr ReturnT consumeComponentValue(
CSSComponentValueDelimiter delimiter,
const VisitorsT&... visitors) {
switch (delimiter) {
case CSSComponentValueDelimiter::Comma:
parser.consumeWhitespace();
if (parser.peek().type() != CSSTokenType::Comma) {
return ReturnT{};
}
if (token->type() == CSSTokenType::EndOfFile) {
return {};
parser.consumeToken();
parser.consumeWhitespace();
break;
case CSSComponentValueDelimiter::Whitespace:
parser.consumeWhitespace();
break;
case CSSComponentValueDelimiter::None:
break;
}

switch (parser.peek().type()) {
case CSSTokenType::Function:
if (auto ret = visitFunction(visitors...)) {
return *ret;
}
function.args.emplace_back(std::move(*token));
continue;
}
break;
case CSSTokenType::OpenParen:
if (auto ret =
visitSimpleBlock(CSSTokenType::CloseParen, visitors...)) {
return *ret;
}
break;
case CSSTokenType::OpenSquare:
if (auto ret =
visitSimpleBlock(CSSTokenType::CloseSquare, visitors...)) {
return *ret;
}
break;
case CSSTokenType::OpenCurly:
if (auto ret =
visitSimpleBlock(CSSTokenType::CloseCurly, visitors...)) {
return *ret;
}
break;
default:
if (auto ret = visitPreservedToken(visitors...)) {
return *ret;
}
break;
}

if (auto func = std::get_if<Function>(&nextValue)) {
function.args.emplace_back(std::move(*func));
continue;
}
return ReturnT{};
}

constexpr ReturnT consumeNextCommaDelimitedValue(
const VisitorsT&... visitors) {
parser.consumeWhitespace();
if (parser.consumeToken().type() != CSSTokenType::Comma) {
return {};
}
parser.consumeWhitespace();
return consumeComponentValue(std::forward<VisitorsT>(visitors)...);
}

return function;
constexpr ReturnT consumeNextWhitespaceDelimitedValue(
const VisitorsT&... visitors) {
parser.consumeWhitespace();
return consumeComponentValue(std::forward<VisitorsT>(visitors)...);
}

CSSTokenizer tokenizer_;
CSSToken currentToken_;
constexpr std::optional<ReturnT> visitFunction(const VisitorsT&... visitors) {
for (auto visitor : {visitors...}) {
if constexpr (CSSFunctionVisitor<decltype(visitor), ReturnT>) {
auto functionValue =
visitor({.name = parser.consumeToken().stringValue()});
parser.consumeWhitespace();
if (parser.peek().type() == CSSTokenType::CloseParen) {
parser.consumeToken();
return functionValue;
}

return {};
}
}

return {};
}

constexpr std::optional<ReturnT> visitSimpleBlock(
CSSTokenType endToken,
const VisitorsT&... visitors) {
for (auto visitor : {visitors...}) {
if constexpr (CSSSimpleBlockVisitor<decltype(visitor), ReturnT>) {
auto blockValue =
visitor({.openBracketType = parser.consumeToken().type()});
parser.consumeWhitespace();
if (parser.peek().type() == endToken) {
parser.consumeToken();
return blockValue;
}

return {};
}
}
return {};
}

constexpr std::optional<ReturnT> visitPreservedToken(
const VisitorsT&... visitors) {
for (auto visitor : {visitors...}) {
if constexpr (CSSPreservedTokenVisitor<decltype(visitor), ReturnT>) {
return visitor(parser.consumeToken());
}
}
return {};
}
};

template <typename ReturnT>
constexpr ReturnT CSSSyntaxParser::consumeComponentValue(
CSSComponentValueDelimiter delimiter,
const CSSComponentValueVisitor<ReturnT> auto&... visitors)
requires(CSSUniqueComponentValueVisitors<ReturnT, decltype(visitors)...>)
{
return CSSComponentValueVisitorDispatcher<ReturnT, decltype(visitors)...>{
*this}
.consumeComponentValue(delimiter, visitors...);
}

template <typename ReturnT>
constexpr ReturnT CSSSyntaxParser::consumeComponentValue(
const CSSComponentValueVisitor<ReturnT> auto&... visitors)
requires(CSSUniqueComponentValueVisitors<ReturnT, decltype(visitors)...>)
{
return consumeComponentValue<ReturnT>(
CSSComponentValueDelimiter::None, visitors...);
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ namespace facebook::react {
* https://www.w3.org/TR/css-syntax-3/#tokenizer-definitions
*/
enum class CSSTokenType {
CloseCurly,
CloseParen,
CloseSquare,
Comma,
Delim,
Dimension,
EndOfFile,
Function,
Ident,
Number,
OpenCurly,
OpenParen,
OpenSquare,
Percentage,
WhiteSpace,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ class CSSTokenizer {
return consumeCharacter(CSSTokenType::OpenParen);
case ')':
return consumeCharacter(CSSTokenType::CloseParen);
case '[':
return consumeCharacter(CSSTokenType::OpenSquare);
case ']':
return consumeCharacter(CSSTokenType::CloseSquare);
case '{':
return consumeCharacter(CSSTokenType::OpenCurly);
case '}':
return consumeCharacter(CSSTokenType::CloseCurly);
case ',':
return consumeCharacter(CSSTokenType::Comma);
case '+':
Expand Down

0 comments on commit 258b481

Please sign in to comment.