/* * 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 #include #include #include #include #include #include #include #include #include #include namespace facebook::react { enum class CSSLinearGradientDirectionKeyword : uint8_t { ToTopLeft, ToTopRight, ToBottomLeft, ToBottomRight, }; struct CSSLinearGradientDirection { // angle or keyword like "to bottom" std::variant value; bool operator==(const CSSLinearGradientDirection &rhs) const = default; }; template <> struct CSSDataTypeParser { static constexpr auto consume(CSSSyntaxParser &parser) -> std::optional { return parseLinearGradientDirection(parser); } private: static constexpr std::optional parseLinearGradientDirection(CSSSyntaxParser &parser) { auto angle = parseNextCSSValue(parser); if (std::holds_alternative(angle)) { return CSSLinearGradientDirection{std::get(angle)}; } auto toResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("to"); }); if (!toResult) { // no direction found, default to 180 degrees (to bottom) return CSSLinearGradientDirection{CSSAngle{180.0f}}; } parser.consumeWhitespace(); std::optional primaryDir; auto primaryResult = parser.consumeComponentValue>( [](const CSSPreservedToken &token) -> std::optional { if (token.type() == CSSTokenType::Ident) { switch (fnv1aLowercase(token.stringValue())) { case fnv1a("top"): return CSSKeyword::Top; case fnv1a("bottom"): return CSSKeyword::Bottom; case fnv1a("left"): return CSSKeyword::Left; case fnv1a("right"): return CSSKeyword::Right; } } return {}; }); if (!primaryResult) { return {}; } primaryDir = primaryResult; parser.consumeWhitespace(); std::optional secondaryDir; auto secondaryResult = parser.consumeComponentValue>( [&](const CSSPreservedToken &token) -> std::optional { if (token.type() == CSSTokenType::Ident) { auto hash = fnv1aLowercase(token.stringValue()); // validate compatible combinations if (primaryDir == CSSKeyword::Top || primaryDir == CSSKeyword::Bottom) { if (hash == fnv1a("left")) { return CSSKeyword::Left; } if (hash == fnv1a("right")) { return CSSKeyword::Right; } } if (primaryDir == CSSKeyword::Left || primaryDir == CSSKeyword::Right) { if (hash == fnv1a("top")) { return CSSKeyword::Top; } if (hash == fnv1a("bottom")) { return CSSKeyword::Bottom; } } } return {}; }); if (secondaryResult) { secondaryDir = secondaryResult; } if (primaryDir == CSSKeyword::Top) { if (secondaryDir == CSSKeyword::Left) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToTopLeft}; } else if (secondaryDir == CSSKeyword::Right) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToTopRight}; } else { // "to top" = 0 degrees return CSSLinearGradientDirection{CSSAngle{0.0f}}; } } else if (primaryDir == CSSKeyword::Bottom) { if (secondaryDir == CSSKeyword::Left) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToBottomLeft}; } else if (secondaryDir == CSSKeyword::Right) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToBottomRight}; } else { // "to bottom" = 180 degrees return CSSLinearGradientDirection{CSSAngle{180.0f}}; } } else if (primaryDir == CSSKeyword::Left) { if (secondaryDir == CSSKeyword::Top) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToTopLeft}; } else if (secondaryDir == CSSKeyword::Bottom) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToBottomLeft}; } else { // "to left" = 270 degrees return CSSLinearGradientDirection{CSSAngle{270.0f}}; } } else if (primaryDir == CSSKeyword::Right) { if (secondaryDir == CSSKeyword::Top) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToTopRight}; } else if (secondaryDir == CSSKeyword::Bottom) { return CSSLinearGradientDirection{CSSLinearGradientDirectionKeyword::ToBottomRight}; } else { // "to right" = 90 degrees return CSSLinearGradientDirection{CSSAngle{90.0f}}; } } return {}; } }; static_assert(CSSDataType); /** * Representation of a color hint (interpolation hint) */ struct CSSColorHint { std::variant position{}; // Support both lengths and percentages bool operator==(const CSSColorHint &rhs) const { return position == rhs.position; } }; template <> struct CSSDataTypeParser { static auto consume(CSSSyntaxParser &parser) -> std::optional { return parseCSSColorHint(parser); } private: static std::optional parseCSSColorHint(CSSSyntaxParser &parser) { auto position = parseNextCSSValue(parser); if (std::holds_alternative(position)) { return CSSColorHint{std::get(position)}; } else if (std::holds_alternative(position)) { return CSSColorHint{std::get(position)}; } return {}; } }; static_assert(CSSDataType); struct CSSColorStop { CSSColor color{}; std::optional> startPosition{}; std::optional> endPosition{}; bool operator==(const CSSColorStop &rhs) const { if (color != rhs.color) { return false; } if (startPosition.has_value() != rhs.startPosition.has_value()) { return false; } if (startPosition.has_value()) { if (startPosition->index() != rhs.startPosition->index()) { return false; } if (*startPosition != *rhs.startPosition) { return false; } } if (endPosition.has_value() != rhs.endPosition.has_value()) { return false; } if (endPosition.has_value()) { if (endPosition->index() != rhs.endPosition->index()) { return false; } if (*endPosition != *rhs.endPosition) { return false; } } return true; } }; template <> struct CSSDataTypeParser { static constexpr auto consume(CSSSyntaxParser &parser) -> std::optional { return parseCSSColorStop(parser); } private: static constexpr std::optional parseCSSColorStop(CSSSyntaxParser &parser) { auto color = parseNextCSSValue(parser); if (!std::holds_alternative(color)) { return {}; } CSSColorStop colorStop; colorStop.color = std::get(color); auto startPosition = parseNextCSSValue(parser, CSSDelimiter::Whitespace); if (std::holds_alternative(startPosition)) { colorStop.startPosition = std::get(startPosition); } else if (std::holds_alternative(startPosition)) { colorStop.startPosition = std::get(startPosition); } if (colorStop.startPosition) { // Try to parse second optional position (supports both lengths and // percentages) auto endPosition = parseNextCSSValue(parser, CSSDelimiter::Whitespace); if (std::holds_alternative(endPosition)) { colorStop.endPosition = std::get(endPosition); } else if (std::holds_alternative(endPosition)) { colorStop.endPosition = std::get(endPosition); } } return colorStop; } }; static_assert(CSSDataType); struct CSSLinearGradientFunction { std::optional direction{}; std::vector> items{}; // Color stops and color hints bool operator==(const CSSLinearGradientFunction &rhs) const = default; static std::pair>, int> parseGradientColorStopsAndHints( CSSSyntaxParser &parser) { std::vector> items; int colorStopCount = 0; std::optional prevColorStop = std::nullopt; do { auto colorStop = parseNextCSSValue(parser); if (std::holds_alternative(colorStop)) { auto parsedColorStop = std::get(colorStop); items.emplace_back(parsedColorStop); prevColorStop = parsedColorStop; colorStopCount++; } else { auto colorHint = parseNextCSSValue(parser); if (std::holds_alternative(colorHint)) { // color hint must be between two color stops if (!prevColorStop) { return {}; } auto nextColorStop = peekNextCSSValue(parser, CSSDelimiter::Comma); if (!std::holds_alternative(nextColorStop)) { return {}; } items.emplace_back(std::get(colorHint)); } else { break; // No more valid items } } } while (parser.consumeDelimiter(CSSDelimiter::Comma)); return {items, colorStopCount}; } }; enum class CSSRadialGradientShape : uint8_t { Circle, Ellipse, }; template <> struct CSSDataTypeParser { static constexpr auto consumePreservedToken(const CSSPreservedToken &token) -> std::optional { if (token.type() == CSSTokenType::Ident) { auto lowercase = fnv1aLowercase(token.stringValue()); if (lowercase == fnv1a("circle")) { return CSSRadialGradientShape::Circle; } else if (lowercase == fnv1a("ellipse")) { return CSSRadialGradientShape::Ellipse; } } return {}; } }; static_assert(CSSDataType); enum class CSSRadialGradientSizeKeyword : uint8_t { ClosestSide, ClosestCorner, FarthestSide, FarthestCorner, }; template <> struct CSSDataTypeParser { static constexpr auto consumePreservedToken(const CSSPreservedToken &token) -> std::optional { if (token.type() == CSSTokenType::Ident) { auto lowercase = fnv1aLowercase(token.stringValue()); if (lowercase == fnv1a("closest-side")) { return CSSRadialGradientSizeKeyword::ClosestSide; } else if (lowercase == fnv1a("closest-corner")) { return CSSRadialGradientSizeKeyword::ClosestCorner; } else if (lowercase == fnv1a("farthest-side")) { return CSSRadialGradientSizeKeyword::FarthestSide; } else if (lowercase == fnv1a("farthest-corner")) { return CSSRadialGradientSizeKeyword::FarthestCorner; } } return {}; } }; static_assert(CSSDataType); struct CSSRadialGradientExplicitSize { std::variant sizeX{}; std::variant sizeY{}; bool operator==(const CSSRadialGradientExplicitSize &rhs) const = default; }; template <> struct CSSDataTypeParser { static auto consume(CSSSyntaxParser &syntaxParser) -> std::optional { auto sizeX = parseNextCSSValue(syntaxParser); if (std::holds_alternative(sizeX)) { return {}; } syntaxParser.consumeWhitespace(); auto sizeY = parseNextCSSValue(syntaxParser); CSSRadialGradientExplicitSize result; if (std::holds_alternative(sizeX)) { result.sizeX = std::get(sizeX); } else { result.sizeX = std::get(sizeX); } if (std::holds_alternative(sizeY) || std::holds_alternative(sizeY)) { if (std::holds_alternative(sizeY)) { result.sizeY = std::get(sizeY); } else { result.sizeY = std::get(sizeY); } } else { result.sizeY = result.sizeX; } return result; } }; static_assert(CSSDataType); using CSSRadialGradientSize = std::variant; struct CSSRadialGradientPosition { std::optional> top{}; std::optional> bottom{}; std::optional> left{}; std::optional> right{}; bool operator==(const CSSRadialGradientPosition &rhs) const { return top == rhs.top && bottom == rhs.bottom && left == rhs.left && right == rhs.right; } }; struct CSSRadialGradientFunction { std::optional shape{}; std::optional size{}; std::optional position{}; std::vector> items{}; // Color stops and color hints bool operator==(const CSSRadialGradientFunction &rhs) const = default; }; template <> struct CSSDataTypeParser { static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) -> std::optional { if (!iequals(func.name, "radial-gradient")) { return {}; } CSSRadialGradientFunction gradient; auto hasExplicitShape = false; auto hasExplicitSingleSize = false; auto shapeResult = parseNextCSSValue(parser); if (std::holds_alternative(shapeResult)) { parser.consumeWhitespace(); } std::optional sizeResult; auto sizeKeywordResult = parseNextCSSValue(parser); if (std::holds_alternative(sizeKeywordResult)) { sizeResult = CSSRadialGradientSize{std::get(sizeKeywordResult)}; parser.consumeWhitespace(); } else { auto explicitSizeResult = parseNextCSSValue(parser); if (std::holds_alternative(explicitSizeResult)) { auto explicitSize = std::get(explicitSizeResult); // negative value validation if (std::holds_alternative(explicitSize.sizeX)) { const auto &lengthX = std::get(explicitSize.sizeX); if (lengthX.value < 0) { return {}; } } else if (std::holds_alternative(explicitSize.sizeX)) { const auto &percentageX = std::get(explicitSize.sizeX); if (percentageX.value < 0) { return {}; } } if (std::holds_alternative(explicitSize.sizeY)) { const auto &lengthY = std::get(explicitSize.sizeY); if (lengthY.value < 0) { return {}; } } else if (std::holds_alternative(explicitSize.sizeY)) { const auto &percentageY = std::get(explicitSize.sizeY); if (percentageY.value < 0) { return {}; } } // check if it's a single size (both X and Y are the same), we use it // to set shape to circle if (explicitSize.sizeX == explicitSize.sizeY) { hasExplicitSingleSize = true; } sizeResult = CSSRadialGradientSize{explicitSize}; parser.consumeWhitespace(); } } if (std::holds_alternative(shapeResult)) { gradient.shape = std::get(shapeResult); hasExplicitShape = true; } else { // default to ellipse gradient.shape = CSSRadialGradientShape::Ellipse; } if (sizeResult.has_value()) { gradient.size = *sizeResult; } else { // default to farthest corner gradient.size = CSSRadialGradientSize{CSSRadialGradientSizeKeyword::FarthestCorner}; } if (!hasExplicitShape && hasExplicitSingleSize) { gradient.shape = CSSRadialGradientShape::Circle; } if (hasExplicitSingleSize && hasExplicitShape && gradient.shape.value() == CSSRadialGradientShape::Ellipse) { // if a single size is explicitly set and the shape is an ellipse do not // apply any gradient. Same as web. return {}; } auto atResult = parser.consumeComponentValue([](const CSSPreservedToken &token) -> bool { return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("at"); }); CSSRadialGradientPosition position; if (atResult) { parser.consumeWhitespace(); std::vector> positionKeywordValues; for (int i = 0; i < 2; i++) { auto keywordFound = false; auto valueFound = false; auto positionKeyword = parser.consumeComponentValue>( [](const CSSPreservedToken &token) -> std::optional { if (token.type() == CSSTokenType::Ident) { auto keyword = std::string(token.stringValue()); auto hash = fnv1aLowercase(keyword); if (hash == fnv1a("top")) { return CSSKeyword::Top; } else if (hash == fnv1a("bottom")) { return CSSKeyword::Bottom; } else if (hash == fnv1a("left")) { return CSSKeyword::Left; } else if (hash == fnv1a("right")) { return CSSKeyword::Right; } else if (hash == fnv1a("center")) { return CSSKeyword::Center; } } return {}; }); if (positionKeyword) { // invalid position declaration of same keyword "at top 10% top 20%" for (const auto &existingValue : positionKeywordValues) { if (std::holds_alternative(existingValue)) { if (std::get(existingValue) == positionKeyword) { return {}; } } } positionKeywordValues.emplace_back(*positionKeyword); keywordFound = true; } parser.consumeWhitespace(); auto lengthPercentageValue = parseNextCSSValue(parser); std::optional value; if (std::holds_alternative(lengthPercentageValue)) { value = std::get(lengthPercentageValue); } else if (std::holds_alternative(lengthPercentageValue)) { value = std::get(lengthPercentageValue); } if (value.has_value()) { positionKeywordValues.emplace_back(*value); valueFound = true; } parser.consumeWhitespace(); if (!keywordFound && !valueFound) { break; } } if (positionKeywordValues.empty()) { return {}; } // 1. [ left | center | right | top | bottom | ] if (positionKeywordValues.size() == 1) { auto value = positionKeywordValues[0]; if (std::holds_alternative(value)) { auto keyword = std::get(value); if (keyword == CSSKeyword::Left) { position.top = CSSPercentage{50.0f}; position.left = CSSPercentage{0.0f}; } else if (keyword == CSSKeyword::Right) { position.top = CSSPercentage{50.0f}; position.left = CSSPercentage{100.0f}; } else if (keyword == CSSKeyword::Top) { position.top = CSSPercentage{0.0f}; position.left = CSSPercentage{50.0f}; } else if (keyword == CSSKeyword::Bottom) { position.top = CSSPercentage{100.0f}; position.left = CSSPercentage{50.0f}; } else if (keyword == CSSKeyword::Center) { position.left = CSSPercentage{50.0f}; position.top = CSSPercentage{50.0f}; } else { return {}; } } else if ((std::holds_alternative(value) || std::holds_alternative(value))) { if (std::holds_alternative(value)) { position.left = std::get(value); } else { position.left = std::get(value); } position.top = CSSPercentage{50.0f}; } else { return {}; } } else if (positionKeywordValues.size() == 2) { auto value1 = positionKeywordValues[0]; auto value2 = positionKeywordValues[1]; // 2. [ left | center | right ] && [ top | center | bottom ] if (std::holds_alternative(value1) && std::holds_alternative(value2)) { auto keyword1 = std::get(value1); auto keyword2 = std::get(value2); auto isHorizontal = [](CSSKeyword kw) { return kw == CSSKeyword::Left || kw == CSSKeyword::Center || kw == CSSKeyword::Right; }; auto isVertical = [](CSSKeyword kw) { return kw == CSSKeyword::Top || kw == CSSKeyword::Center || kw == CSSKeyword::Bottom; }; if (isHorizontal(keyword1) && isVertical(keyword2)) { // First horizontal, second vertical if (keyword1 == CSSKeyword::Left) { position.left = CSSPercentage{0.0f}; } else if (keyword1 == CSSKeyword::Right) { position.right = CSSPercentage{0.0f}; } else if (keyword1 == CSSKeyword::Center) { position.left = CSSPercentage{50.0f}; } if (keyword2 == CSSKeyword::Top) { position.top = CSSPercentage{0.0f}; } else if (keyword2 == CSSKeyword::Bottom) { position.bottom = CSSPercentage{0.0f}; } else if (keyword2 == CSSKeyword::Center) { position.top = CSSPercentage{50.0f}; } } else if (isVertical(keyword1) && isHorizontal(keyword2)) { // First vertical, second horizontal if (keyword1 == CSSKeyword::Top) { position.top = CSSPercentage{0.0f}; } else if (keyword1 == CSSKeyword::Bottom) { position.bottom = CSSPercentage{0.0f}; } else if (keyword1 == CSSKeyword::Center) { position.top = CSSPercentage{50.0f}; } if (keyword2 == CSSKeyword::Left) { position.left = CSSPercentage{0.0f}; } else if (keyword2 == CSSKeyword::Right) { position.left = CSSPercentage{100.0f}; } else if (keyword2 == CSSKeyword::Center) { position.left = CSSPercentage{50.0f}; } } else { return {}; } } // 3. [ left | center | right | ] [ top | center | // bottom | ] else { if (std::holds_alternative(value1)) { auto keyword1 = std::get(value1); if (keyword1 == CSSKeyword::Left) { position.left = CSSPercentage{0.0f}; } else if (keyword1 == CSSKeyword::Right) { position.right = CSSPercentage{0.0f}; } else if (keyword1 == CSSKeyword::Center) { position.left = CSSPercentage{50.0f}; } else { return {}; } } else if ((std::holds_alternative(value1) || std::holds_alternative(value1))) { if (std::holds_alternative(value1)) { position.left = std::get(value1); } else { position.left = std::get(value1); } } else { return {}; } if (std::holds_alternative(value2)) { auto keyword2 = std::get(value2); if (keyword2 == CSSKeyword::Top) { position.top = CSSPercentage{0.0f}; } else if (keyword2 == CSSKeyword::Bottom) { position.bottom = CSSPercentage{0.f}; } else if (keyword2 == CSSKeyword::Center) { position.top = CSSPercentage{50.0f}; } else { return {}; } } else if ((std::holds_alternative(value2) || std::holds_alternative(value2))) { if (std::holds_alternative(value2)) { position.top = std::get(value2); } else { position.top = std::get(value2); } } else { return {}; } } } // 4. [ [ left | right ] ] && [ [ top | bottom ] // ] else if (positionKeywordValues.size() == 4) { auto value1 = positionKeywordValues[0]; auto value2 = positionKeywordValues[1]; auto value3 = positionKeywordValues[2]; auto value4 = positionKeywordValues[3]; if (!std::holds_alternative(value1)) { return {}; } if (!std::holds_alternative(value3)) { return {}; } if ((!std::holds_alternative(value2) && !std::holds_alternative(value2))) { return {}; } if ((!std::holds_alternative(value4) && !std::holds_alternative(value4))) { return {}; } auto parsedValue2 = std::holds_alternative(value2) ? std::variant{std::get(value2)} : std::variant{std::get(value2)}; auto parsedValue4 = std::holds_alternative(value4) ? std::variant{std::get(value4)} : std::variant{std::get(value4)}; auto keyword1 = std::get(value1); auto keyword3 = std::get(value3); if (keyword1 == CSSKeyword::Left) { position.left = parsedValue2; } else if (keyword1 == CSSKeyword::Right) { position.right = parsedValue2; } else if (keyword1 == CSSKeyword::Top) { position.top = parsedValue2; } else if (keyword1 == CSSKeyword::Bottom) { position.bottom = parsedValue2; } else { return {}; } if (keyword3 == CSSKeyword::Left) { position.left = parsedValue4; } else if (keyword3 == CSSKeyword::Right) { position.right = parsedValue4; } else if (keyword3 == CSSKeyword::Top) { position.top = parsedValue4; } else if (keyword3 == CSSKeyword::Bottom) { position.bottom = parsedValue4; } else { return {}; } } else { return {}; } gradient.position = position; } else { // Default position position.top = CSSPercentage{50.0f}; position.left = CSSPercentage{50.0f}; gradient.position = position; } parser.consumeDelimiter(CSSDelimiter::Comma); auto [items, colorStopCount] = CSSLinearGradientFunction::parseGradientColorStopsAndHints(parser); if (items.empty() || colorStopCount < 2) { return {}; } gradient.items = std::move(items); return gradient; } }; static_assert(CSSDataType); template <> struct CSSDataTypeParser { static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser) -> std::optional { if (!iequals(func.name, "linear-gradient")) { return {}; } CSSLinearGradientFunction gradient; auto parsedDirection = parseNextCSSValue(parser); if (!std::holds_alternative(parsedDirection)) { return {}; } parser.consumeDelimiter(CSSDelimiter::Comma); gradient.direction = std::get(parsedDirection); auto [items, colorStopCount] = CSSLinearGradientFunction::parseGradientColorStopsAndHints(parser); if (items.empty() || colorStopCount < 2) { return {}; } gradient.items = std::move(items); return gradient; } }; static_assert(CSSDataType); /** * Representation of * https://www.w3.org/TR/css-backgrounds-3/#background-image */ using CSSBackgroundImage = CSSCompoundDataType; /** * Variant of possible CSS background image types */ using CSSBackgroundImageVariant = CSSVariantWithTypes; /** * Representation of */ using CSSBackgroundImageList = CSSCommaSeparatedList; } // namespace facebook::react