/* * 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 { class CSSSyntaxParser; /** * 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{}; }; /** * 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 * component value list. It is expected that the visitor finishes before the end * of the function block. */ template concept CSSFunctionVisitor = CSSSyntaxVisitorReturn && requires(T visitor, CSSFunctionBlock func, CSSSyntaxParser& blockParser) { { visitor(func, blockParser) } -> std::convertible_to; }; /** * A CSSPreservedTokenVisitor is called after parsing a preserved token * component value. */ template concept CSSPreservedTokenVisitor = CSSSyntaxVisitorReturn && requires(T visitor, CSSPreservedToken token) { { visitor(token) } -> std::convertible_to; }; /** * 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 concept CSSSimpleBlockVisitor = CSSSyntaxVisitorReturn && requires(T visitor, CSSSimpleBlock block, CSSSyntaxParser& blockParser) { { visitor(block, blockParser) } -> std::convertible_to; }; /** * Any visitor for a component value. */ template 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 = CSSSyntaxVisitorReturn && (CSSComponentValueVisitor && ...) && ((CSSFunctionVisitor ? 1 : 0) + ... + 0) <= 1 && ((CSSPreservedTokenVisitor ? 1 : 0) + ... + 0) <= 1 && ((CSSSimpleBlockVisitor ? 1 : 0) + ... + 0) <= 1; /** * Describes the delimeter to expect before the next component value. */ enum class CSSDelimiter { Whitespace, OptionalWhitespace, Solidus, SolidusOrWhitespace, Comma, CommaOrWhitespace, None, }; /** * CSSSyntaxParser allows parsing streams of CSS text into "component * values". * * https://www.w3.org/TR/css-syntax-3/#component-value */ class CSSSyntaxParser { template < CSSSyntaxVisitorReturn ReturnT, CSSComponentValueVisitor... 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. 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. * * 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 * 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 consumeComponentValue( CSSDelimiter delimiter, const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors); template constexpr ReturnT consumeComponentValue( const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors); /** * The parser is considered finished when there are no more remaining tokens * to be processed */ constexpr bool isFinished() const { return currentToken_.type() == CSSTokenType::EndOfFile; } /** * Consume any whitespace tokens. */ constexpr void consumeWhitespace() { if (currentToken_.type() == CSSTokenType::WhiteSpace) { currentToken_ = tokenizer_.next(); } } /** * 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_}, currentToken_{parser.currentToken_}, terminator_{terminator} {} constexpr const CSSToken& peek() const { return currentToken_; } constexpr CSSToken consumeToken() { auto prevToken = currentToken_; currentToken_ = tokenizer_.next(); return prevToken; } constexpr void advanceToBlockParser(CSSSyntaxParser& blockParser) { currentToken_ = blockParser.currentToken_; tokenizer_ = blockParser.tokenizer_; } CSSTokenizer tokenizer_; CSSToken currentToken_; CSSTokenType terminator_{CSSTokenType::EndOfFile}; }; template < CSSSyntaxVisitorReturn ReturnT, CSSComponentValueVisitor... VisitorsT> struct CSSComponentValueVisitorDispatcher { CSSSyntaxParser& parser; constexpr ReturnT consumeComponentValue( CSSDelimiter delimiter, const VisitorsT&... visitors) { auto originalParser = parser; if (!parser.consumeDelimiter(delimiter)) { parser = originalParser; return {}; } if (parser.peek().type() == parser.terminator_) { parser = originalParser; return {}; } switch (parser.peek().type()) { case CSSTokenType::Function: if (auto ret = visitFunction(visitors...)) { return *ret; } 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; } parser = originalParser; return ReturnT{}; } constexpr std::optional visitFunction( const CSSComponentValueVisitor auto& visitor, const CSSComponentValueVisitor auto&... rest) { if constexpr (CSSFunctionVisitor) { auto name = parser.consumeToken().stringValue(); // CSS syntax spec says whitespace is a preserved token, but CSS values // spec allows whitespace around parens for all function notation, so we // allow this to let the visitors not need to deal with leading/trailing // whitespace. https://www.w3.org/TR/css-values-3/#functional-notations parser.consumeWhitespace(); auto blockParser = CSSSyntaxParser{parser, CSSTokenType::CloseParen /*terminator*/}; auto functionValue = visitor({name}, blockParser); parser.advanceToBlockParser(blockParser); parser.consumeWhitespace(); if (parser.peek().type() == CSSTokenType::CloseParen && functionValue != ReturnT{}) { parser.consumeToken(); return functionValue; } return {}; } return visitFunction(rest...); } constexpr std::optional visitFunction() { return {}; } constexpr std::optional visitSimpleBlock( CSSTokenType endToken, const CSSComponentValueVisitor auto& visitor, const CSSComponentValueVisitor auto&... rest) { if constexpr (CSSSimpleBlockVisitor) { auto openBracketType = parser.consumeToken().type(); parser.consumeWhitespace(); auto blockParser = CSSSyntaxParser{parser, endToken}; auto blockValue = visitor({openBracketType}, blockParser); parser.advanceToBlockParser(blockParser); parser.consumeWhitespace(); if (parser.peek().type() == endToken && blockValue != ReturnT{}) { parser.consumeToken(); return blockValue; } return {}; } return visitSimpleBlock(endToken, rest...); } constexpr std::optional visitSimpleBlock(CSSTokenType endToken) { return {}; } constexpr std::optional visitPreservedToken( const CSSComponentValueVisitor auto& visitor, const CSSComponentValueVisitor auto&... rest) { if constexpr (CSSPreservedTokenVisitor) { auto ret = visitor(parser.consumeToken()); if (ret != ReturnT{}) { return ret; } } return visitPreservedToken(rest...); } constexpr std::optional visitPreservedToken() { return {}; } }; template constexpr ReturnT CSSSyntaxParser::consumeComponentValue( CSSDelimiter delimiter, const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors) { return CSSComponentValueVisitorDispatcher{ *this} .consumeComponentValue(delimiter, visitors...); } template constexpr ReturnT CSSSyntaxParser::consumeComponentValue( const CSSComponentValueVisitor auto&... visitors) requires(CSSUniqueComponentValueVisitors) { return consumeComponentValue(CSSDelimiter::None, visitors...); } } // namespace facebook::react