diff --git a/fixtures/html/simple.html b/fixtures/html/simple.html index da0c58fe8..5ef2045ab 100644 --- a/fixtures/html/simple.html +++ b/fixtures/html/simple.html @@ -14,12 +14,12 @@ img.float-image { border-bottom-right-radius: 50px; height: 300px; - transform: translate3d(0, 0, 35px); + transform: translate3d(0, 0, 0); transition: transform 0.3s ease-in-out; } img.float-image:hover { - transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 35px); } #header { diff --git a/src/client/cssom/parsers/css_transform_parser.cpp b/src/client/cssom/parsers/css_transform_parser.cpp new file mode 100644 index 000000000..31f982566 --- /dev/null +++ b/src/client/cssom/parsers/css_transform_parser.cpp @@ -0,0 +1,1079 @@ +#include "./css_transform_parser.hpp" +#include +#include +#include +#include + +namespace client_cssom::css_transform_parser +{ + using namespace std; + using namespace css_value_tokenizer; + + CSSTransformParser::CSSTransformParser(const string &input) + : input_(input) + , tokenizer_(input) + , current_token_index_(0) + , is_valid_(false) + { + tokens_ = tokenizer_.tokenize(); + } + + vector CSSTransformParser::parse() + { + vector functions; + current_token_index_ = 0; + is_valid_ = true; + error_message_.clear(); + + // Handle 'none' case + if (tokens_.size() == 1 && tokens_[0].type == TokenType::kIdentifier && tokens_[0].value == "none") + { + return functions; // Return empty list for 'none' + } + + // Parse transform functions + while (!isAtEnd()) + { + auto func = parseTransformFunction(); + if (!func.has_value()) + { + is_valid_ = false; + return functions; + } + functions.push_back(func.value()); + + // Skip whitespace between functions + while (!isAtEnd() && currentToken().type == TokenType::kWhitespace) + { + advance(); + } + } + + return functions; + } + + optional CSSTransformParser::parseTransformFunction() + { + if (isAtEnd() || currentToken().type != TokenType::kFunction) + { + setError("Expected transform function"); + return nullopt; + } + + const string &function_name = currentToken().value; + TransformFunctionType type = getFunctionType(function_name); + + advance(); // Skip function name + + switch (type) + { + case TransformFunctionType::kMatrix: + return parseMatrix(); + case TransformFunctionType::kMatrix3D: + return parseMatrix3D(); + case TransformFunctionType::kTranslate: + return parseTranslate(); + case TransformFunctionType::kTranslateX: + return parseTranslateX(); + case TransformFunctionType::kTranslateY: + return parseTranslateY(); + case TransformFunctionType::kTranslateZ: + return parseTranslateZ(); + case TransformFunctionType::kTranslate3D: + return parseTranslate3D(); + case TransformFunctionType::kScale: + return parseScale(); + case TransformFunctionType::kScaleX: + return parseScaleX(); + case TransformFunctionType::kScaleY: + return parseScaleY(); + case TransformFunctionType::kScaleZ: + return parseScaleZ(); + case TransformFunctionType::kScale3D: + return parseScale3D(); + case TransformFunctionType::kRotate: + return parseRotate(); + case TransformFunctionType::kRotateX: + return parseRotateX(); + case TransformFunctionType::kRotateY: + return parseRotateY(); + case TransformFunctionType::kRotateZ: + return parseRotateZ(); + case TransformFunctionType::kRotate3D: + return parseRotate3D(); + case TransformFunctionType::kSkew: + return parseSkew(); + case TransformFunctionType::kSkewX: + return parseSkewX(); + case TransformFunctionType::kSkewY: + return parseSkewY(); + case TransformFunctionType::kPerspective: + return parsePerspective(); + default: + setError("Unknown transform function: " + function_name); + return nullopt; + } + } + + optional CSSTransformParser::parseMatrix() + { + TransformFunction func(TransformFunctionType::kMatrix); + + // matrix(a, b, c, d, e, f) - 6 numbers + for (int i = 0; i < 6; ++i) + { + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in matrix()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (i < 5 && !consumeComma()) + { + setError("Expected comma in matrix()"); + return nullopt; + } + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in matrix()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseMatrix3D() + { + TransformFunction func(TransformFunctionType::kMatrix3D); + + // matrix3d(m11, m12, ..., m44) - 16 numbers + for (int i = 0; i < 16; ++i) + { + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in matrix3d()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (i < 15 && !consumeComma()) + { + setError("Expected comma in matrix3d()"); + return nullopt; + } + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in matrix3d()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseTranslate() + { + TransformFunction func(TransformFunctionType::kTranslate); + + // translate(x, y?) - 1 or 2 length/percentage values + double value; + string unit; + + // X value (required) + if (!consumeLength(value, unit)) + { + setError("Expected length/percentage in translate()"); + return nullopt; + } + func.values.push_back(value); + func.units.push_back(unit); + + // Y value (optional, defaults to 0) + if (!isAtEnd() && currentToken().type == TokenType::kComma) + { + advance(); // Skip comma + + if (!consumeLength(value, unit)) + { + setError("Expected length/percentage for Y in translate()"); + return nullopt; + } + func.values.push_back(value); + func.units.push_back(unit); + } + else + { + // Default Y to 0 + func.values.push_back(0.0); + func.units.push_back("px"); + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in translate()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseTranslateX() + { + TransformFunction func(TransformFunctionType::kTranslateX); + + double value; + string unit; + + if (!consumeLength(value, unit)) + { + setError("Expected length/percentage in translateX()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in translateX()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseTranslateY() + { + TransformFunction func(TransformFunctionType::kTranslateY); + + double value; + string unit; + + if (!consumeLength(value, unit)) + { + setError("Expected length/percentage in translateY()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in translateY()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseTranslateZ() + { + TransformFunction func(TransformFunctionType::kTranslateZ); + + double value; + string unit; + + if (!consumeLength(value, unit)) + { + setError("Expected length in translateZ()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in translateZ()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseTranslate3D() + { + TransformFunction func(TransformFunctionType::kTranslate3D); + + // translate3d(x, y, z) - 3 length values + for (int i = 0; i < 3; ++i) + { + double value; + string unit; + + if (!consumeLength(value, unit)) + { + setError("Expected length in translate3d()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (i < 2 && !consumeComma()) + { + setError("Expected comma in translate3d()"); + return nullopt; + } + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in translate3d()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseScale() + { + TransformFunction func(TransformFunctionType::kScale); + + // scale(x, y?) - 1 or 2 numbers + double value; + string unit; + + // X value (required) + if (!consumeNumber(value, unit)) + { + setError("Expected number in scale()"); + return nullopt; + } + func.values.push_back(value); + func.units.push_back(unit); + + // Y value (optional, defaults to x) + if (!isAtEnd() && currentToken().type == TokenType::kComma) + { + advance(); // Skip comma + + if (!consumeNumber(value, unit)) + { + setError("Expected number for Y in scale()"); + return nullopt; + } + func.values.push_back(value); + func.units.push_back(unit); + } + else + { + // Default Y to X value + func.values.push_back(func.values[0]); + func.units.push_back(func.units[0]); + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in scale()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseScaleX() + { + TransformFunction func(TransformFunctionType::kScaleX); + + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in scaleX()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in scaleX()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseScaleY() + { + TransformFunction func(TransformFunctionType::kScaleY); + + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in scaleY()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in scaleY()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseScaleZ() + { + TransformFunction func(TransformFunctionType::kScaleZ); + + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in scaleZ()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in scaleZ()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseScale3D() + { + TransformFunction func(TransformFunctionType::kScale3D); + + // scale3d(x, y, z) - 3 numbers + for (int i = 0; i < 3; ++i) + { + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in scale3d()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (i < 2 && !consumeComma()) + { + setError("Expected comma in scale3d()"); + return nullopt; + } + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in scale3d()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseRotate() + { + TransformFunction func(TransformFunctionType::kRotate); + + double value; + string unit; + + if (!consumeAngle(value, unit)) + { + setError("Expected angle in rotate()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in rotate()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseRotateX() + { + TransformFunction func(TransformFunctionType::kRotateX); + + double value; + string unit; + + if (!consumeAngle(value, unit)) + { + setError("Expected angle in rotateX()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in rotateX()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseRotateY() + { + TransformFunction func(TransformFunctionType::kRotateY); + + double value; + string unit; + + if (!consumeAngle(value, unit)) + { + setError("Expected angle in rotateY()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in rotateY()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseRotateZ() + { + TransformFunction func(TransformFunctionType::kRotateZ); + + double value; + string unit; + + if (!consumeAngle(value, unit)) + { + setError("Expected angle in rotateZ()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in rotateZ()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseRotate3D() + { + TransformFunction func(TransformFunctionType::kRotate3D); + + // rotate3d(x, y, z, angle) - 3 numbers + 1 angle + for (int i = 0; i < 3; ++i) + { + double value; + string unit; + + if (!consumeNumber(value, unit)) + { + setError("Expected number in rotate3d()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeComma()) + { + setError("Expected comma in rotate3d()"); + return nullopt; + } + } + + // Angle parameter + double value; + string unit; + if (!consumeAngle(value, unit)) + { + setError("Expected angle in rotate3d()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in rotate3d()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseSkew() + { + TransformFunction func(TransformFunctionType::kSkew); + + // skew(x, y?) - 1 or 2 angles + double value; + string unit; + + // X angle (required) + if (!consumeAngle(value, unit)) + { + setError("Expected angle in skew()"); + return nullopt; + } + func.values.push_back(value); + func.units.push_back(unit); + + // Y angle (optional, defaults to 0) + if (!isAtEnd() && currentToken().type == TokenType::kComma) + { + advance(); // Skip comma + + if (!consumeAngle(value, unit)) + { + setError("Expected angle for Y in skew()"); + return nullopt; + } + func.values.push_back(value); + func.units.push_back(unit); + } + else + { + // Default Y to 0 + func.values.push_back(0.0); + func.units.push_back("deg"); + } + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in skew()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseSkewX() + { + TransformFunction func(TransformFunctionType::kSkewX); + + double value; + string unit; + + if (!consumeAngle(value, unit)) + { + setError("Expected angle in skewX()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in skewX()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parseSkewY() + { + TransformFunction func(TransformFunctionType::kSkewY); + + double value; + string unit; + + if (!consumeAngle(value, unit)) + { + setError("Expected angle in skewY()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in skewY()"); + return nullopt; + } + + return func; + } + + optional CSSTransformParser::parsePerspective() + { + TransformFunction func(TransformFunctionType::kPerspective); + + double value; + string unit; + + if (!consumeLength(value, unit)) + { + setError("Expected length in perspective()"); + return nullopt; + } + + func.values.push_back(value); + func.units.push_back(unit); + + if (!consumeToken(TokenType::kRightParen)) + { + setError("Expected closing parenthesis in perspective()"); + return nullopt; + } + + return func; + } + + // Helper methods + bool CSSTransformParser::consumeToken(TokenType expected_type) + { + if (isAtEnd() || currentToken().type != expected_type) + { + return false; + } + advance(); + return true; + } + + bool CSSTransformParser::consumeComma() + { + return consumeToken(TokenType::kComma); + } + + bool CSSTransformParser::consumeNumber(double &value, string &unit) + { + if (isAtEnd()) + { + return false; + } + + const Token &token = currentToken(); + + if (token.type == TokenType::kNumber) + { + value = token.numeric_value; + unit = ""; + advance(); + return true; + } + else if (token.type == TokenType::kDimension) + { + value = token.numeric_value; + unit = token.unit; + advance(); + return true; + } + else if (token.type == TokenType::kPercentage) + { + value = token.numeric_value; + unit = "%"; + advance(); + return true; + } + else if (token.type == TokenType::kIdentifier) + { + // Handle negative numbers tokenized as identifiers (e.g., "-10", "-1") + const string &str = token.value; + if (!str.empty() && str[0] == '-') + { + try + { + // Try to parse as a pure number + double parsed_value = stod(str); + value = parsed_value; + unit = ""; + advance(); + + // Check if next token is a decimal part (e.g., ".5" after "-0") + if (!isAtEnd() && currentToken().type == TokenType::kNumber) + { + const string &next_str = currentToken().value; + if (!next_str.empty() && next_str[0] == '.') + { + // Combine the integer and decimal parts + double decimal_part = currentToken().numeric_value; + // For negative identifiers starting with "-", always treat as negative + if (str[0] == '-') + { + value = parsed_value - decimal_part; + } + else + { + value = parsed_value + decimal_part; + } + advance(); // Consume the decimal part + } + } + + return true; + } + catch (...) + { + // Not a valid number, fall through + } + } + } + + return false; + } + + bool CSSTransformParser::consumeLength(double &value, string &unit) + { + if (isAtEnd()) + { + return false; + } + + const Token &token = currentToken(); + + if (token.type == TokenType::kNumber && token.numeric_value == 0.0) + { + // Zero is allowed without unit for lengths + value = 0.0; + unit = "px"; + advance(); + return true; + } + else if (token.type == TokenType::kDimension) + { + // Check if unit is a valid length unit + const string &u = token.unit; + if (u == "px" || u == "em" || u == "rem" || u == "vh" || u == "vw" || + u == "vmin" || u == "vmax" || u == "%" || u == "cm" || u == "mm" || + u == "in" || u == "pt" || u == "pc") + { + value = token.numeric_value; + unit = u; + advance(); + return true; + } + } + else if (token.type == TokenType::kPercentage) + { + value = token.numeric_value; + unit = "%"; + advance(); + return true; + } + else if (token.type == TokenType::kIdentifier) + { + // Handle negative lengths tokenized as identifiers (e.g., "-10px", "-5em") + const string &str = token.value; + if (!str.empty() && str[0] == '-') + { + // Try to extract number and unit + size_t unit_start = 1; // Start after the minus sign + while (unit_start < str.length() && + (isdigit(str[unit_start]) || str[unit_start] == '.')) + { + unit_start++; + } + + if (unit_start > 1 && unit_start < str.length()) + { + try + { + string number_part = str.substr(0, unit_start); + string unit_part = str.substr(unit_start); + + // Check if unit is valid + if (unit_part == "px" || unit_part == "em" || unit_part == "rem" || + unit_part == "vh" || unit_part == "vw" || unit_part == "vmin" || + unit_part == "vmax" || unit_part == "%" || unit_part == "cm" || + unit_part == "mm" || unit_part == "in" || unit_part == "pt" || + unit_part == "pc") + { + double parsed_value = stod(number_part); + value = parsed_value; + unit = unit_part; + advance(); + return true; + } + } + catch (...) + { + // Not a valid number, fall through + } + } + } + } + + return false; + } + + bool CSSTransformParser::consumeAngle(double &value, string &unit) + { + if (isAtEnd()) + { + return false; + } + + const Token &token = currentToken(); + + if (token.type == TokenType::kNumber && token.numeric_value == 0.0) + { + // Zero is allowed without unit for angles + value = 0.0; + unit = "deg"; + advance(); + return true; + } + else if (token.type == TokenType::kDimension) + { + // Check if unit is a valid angle unit + const string &u = token.unit; + if (u == "deg" || u == "rad" || u == "grad" || u == "turn") + { + value = token.numeric_value; + unit = u; + advance(); + return true; + } + } + else if (token.type == TokenType::kIdentifier) + { + // Handle negative angles tokenized as identifiers (e.g., "-45deg", "-1.5rad") + const string &str = token.value; + if (!str.empty() && str[0] == '-') + { + // Try to extract number and unit + size_t unit_start = 1; // Start after the minus sign + while (unit_start < str.length() && + (isdigit(str[unit_start]) || str[unit_start] == '.')) + { + unit_start++; + } + + if (unit_start > 1 && unit_start < str.length()) + { + try + { + string number_part = str.substr(0, unit_start); + string unit_part = str.substr(unit_start); + + // Check if unit is valid + if (unit_part == "deg" || unit_part == "rad" || unit_part == "grad" || unit_part == "turn") + { + double parsed_value = stod(number_part); + value = parsed_value; + unit = unit_part; + advance(); + return true; + } + } + catch (...) + { + // Not a valid number, fall through + } + } + } + } + + return false; + } + + bool CSSTransformParser::isAtEnd() const + { + return current_token_index_ >= tokens_.size(); + } + + const Token &CSSTransformParser::currentToken() const + { + static Token dummy_token(TokenType::kWhitespace); + if (isAtEnd()) + { + return dummy_token; + } + return tokens_[current_token_index_]; + } + + void CSSTransformParser::advance() + { + if (!isAtEnd()) + { + current_token_index_++; + } + } + + void CSSTransformParser::setError(const string &message) + { + error_message_ = message; + is_valid_ = false; + } + + TransformFunctionType CSSTransformParser::getFunctionType(const string &name) + { + static const unordered_map function_map = { + {"matrix", TransformFunctionType::kMatrix}, + {"matrix3d", TransformFunctionType::kMatrix3D}, + {"translate", TransformFunctionType::kTranslate}, + {"translateX", TransformFunctionType::kTranslateX}, + {"translateY", TransformFunctionType::kTranslateY}, + {"translateZ", TransformFunctionType::kTranslateZ}, + {"translate3d", TransformFunctionType::kTranslate3D}, + {"scale", TransformFunctionType::kScale}, + {"scaleX", TransformFunctionType::kScaleX}, + {"scaleY", TransformFunctionType::kScaleY}, + {"scaleZ", TransformFunctionType::kScaleZ}, + {"scale3d", TransformFunctionType::kScale3D}, + {"rotate", TransformFunctionType::kRotate}, + {"rotateX", TransformFunctionType::kRotateX}, + {"rotateY", TransformFunctionType::kRotateY}, + {"rotateZ", TransformFunctionType::kRotateZ}, + {"rotate3d", TransformFunctionType::kRotate3D}, + {"skew", TransformFunctionType::kSkew}, + {"skewX", TransformFunctionType::kSkewX}, + {"skewY", TransformFunctionType::kSkewY}, + {"perspective", TransformFunctionType::kPerspective}}; + + auto it = function_map.find(name); + if (it != function_map.end()) + { + return it->second; + } + + // Return a default, error will be handled by caller + return TransformFunctionType::kMatrix; + } +} diff --git a/src/client/cssom/parsers/css_transform_parser.hpp b/src/client/cssom/parsers/css_transform_parser.hpp new file mode 100644 index 000000000..ba2d0085c --- /dev/null +++ b/src/client/cssom/parsers/css_transform_parser.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include "./css_value_tokenizer.hpp" + +namespace client_cssom::css_transform_parser +{ + // Enum for different transform function types + enum class TransformFunctionType + { + kMatrix, + kMatrix3D, + kTranslate, + kTranslateX, + kTranslateY, + kTranslateZ, + kTranslate3D, + kScale, + kScaleX, + kScaleY, + kScaleZ, + kScale3D, + kRotate, + kRotateX, + kRotateY, + kRotateZ, + kRotate3D, + kSkew, + kSkewX, + kSkewY, + kPerspective + }; + + // Structure to hold parsed transform function data + struct TransformFunction + { + TransformFunctionType type; + std::vector values; // Numeric values + std::vector units; // Units for each value + + TransformFunction(TransformFunctionType t) + : type(t) + { + } + }; + + // Main parser class + class CSSTransformParser + { + public: + explicit CSSTransformParser(const std::string &input); + + // Parse the transform string and return list of functions + std::vector parse(); + + // Check if parsing was successful + bool isValid() const + { + return is_valid_; + } + + // Get error message if parsing failed + const std::string &getError() const + { + return error_message_; + } + + private: + std::string input_; + css_value_tokenizer::CSSValueTokenizer tokenizer_; + std::vector tokens_; + size_t current_token_index_; + bool is_valid_; + std::string error_message_; + + // Parse individual transform functions + std::optional parseTransformFunction(); + std::optional parseMatrix(); + std::optional parseMatrix3D(); + std::optional parseTranslate(); + std::optional parseTranslateX(); + std::optional parseTranslateY(); + std::optional parseTranslateZ(); + std::optional parseTranslate3D(); + std::optional parseScale(); + std::optional parseScaleX(); + std::optional parseScaleY(); + std::optional parseScaleZ(); + std::optional parseScale3D(); + std::optional parseRotate(); + std::optional parseRotateX(); + std::optional parseRotateY(); + std::optional parseRotateZ(); + std::optional parseRotate3D(); + std::optional parseSkew(); + std::optional parseSkewX(); + std::optional parseSkewY(); + std::optional parsePerspective(); + + // Helper methods + bool consumeToken(css_value_tokenizer::TokenType expected_type); + bool consumeComma(); + bool consumeNumber(double &value, std::string &unit); + bool consumeLength(double &value, std::string &unit); + bool consumeAngle(double &value, std::string &unit); + bool isAtEnd() const; + const css_value_tokenizer::Token ¤tToken() const; + void advance(); + void setError(const std::string &message); + + // Transform function name to type mapping + static TransformFunctionType getFunctionType(const std::string &name); + }; +} diff --git a/src/client/cssom/values/specified/angle.hpp b/src/client/cssom/values/specified/angle.hpp index a8da72a81..d3d8adbc0 100644 --- a/src/client/cssom/values/specified/angle.hpp +++ b/src/client/cssom/values/specified/angle.hpp @@ -130,6 +130,22 @@ namespace client_cssom::values::specified { return Angle(AngleDimension::Deg(0.0f), false); } + static Angle Deg(float value) + { + return Angle(AngleDimension::Deg(value), false); + } + static Angle Grad(float value) + { + return Angle(AngleDimension::Grad(value), false); + } + static Angle Rad(float value) + { + return Angle(AngleDimension::Rad(value), false); + } + static Angle Turn(float value) + { + return Angle(AngleDimension::Turn(value), false); + } public: Angle() = default; diff --git a/src/client/cssom/values/specified/length.hpp b/src/client/cssom/values/specified/length.hpp index f23caae95..a8c98cb3f 100644 --- a/src/client/cssom/values/specified/length.hpp +++ b/src/client/cssom/values/specified/length.hpp @@ -514,8 +514,6 @@ namespace client_cssom::values::specified , length_(AbsoluteLength::Px(0)) { } - - private: NoCalcLength(AbsoluteLength absolute_length) : tag_(kAbsolute) , length_(absolute_length) @@ -889,7 +887,6 @@ namespace client_cssom::values::specified { } - private: LengthPercentage(NoCalcLength length) : tag_(kLength) , value_(length) diff --git a/src/client/cssom/values/specified/transform.hpp b/src/client/cssom/values/specified/transform.hpp index 68a2ac9e7..b2cd85e1e 100644 --- a/src/client/cssom/values/specified/transform.hpp +++ b/src/client/cssom/values/specified/transform.hpp @@ -4,7 +4,11 @@ #include #include #include +#include #include +#include +#include +#include namespace client_cssom::values::specified { @@ -98,6 +102,61 @@ namespace client_cssom::values::specified specified_translate_3d.y().toComputedValue(context), specified_translate_3d.z().toComputedValue(context)); } + else if (isScale()) + { + const auto &specified_scale = getScale(); + return computed::TransformOperation::Scale(specified_scale.number().toComputedValue(context), specified_scale.number().toComputedValue(context)); + } + else if (isScaleX()) + { + const auto &specified_scale_x = getScaleX(); + return computed::TransformOperation::ScaleX(specified_scale_x.x().toComputedValue(context)); + } + else if (isScaleY()) + { + const auto &specified_scale_y = getScaleY(); + return computed::TransformOperation::ScaleY(specified_scale_y.y().toComputedValue(context)); + } + else if (isScaleZ()) + { + const auto &specified_scale_z = getScaleZ(); + return computed::TransformOperation::ScaleZ(specified_scale_z.z().toComputedValue(context)); + } + else if (isScale3D()) + { + const auto &specified_scale_3d = getScale3D(); + return computed::TransformOperation::Scale3D(specified_scale_3d.x().toComputedValue(context), + specified_scale_3d.y().toComputedValue(context), + specified_scale_3d.z().toComputedValue(context)); + } + else if (isRotate()) + { + const auto &specified_rotate = getRotate(); + return computed::TransformOperation::Rotate(specified_rotate.angle().toComputedValue(context)); + } + else if (isRotateX()) + { + const auto &specified_rotate_x = getRotateX(); + return computed::TransformOperation::RotateX(specified_rotate_x.angle().toComputedValue(context)); + } + else if (isRotateY()) + { + const auto &specified_rotate_y = getRotateY(); + return computed::TransformOperation::RotateY(specified_rotate_y.angle().toComputedValue(context)); + } + else if (isRotateZ()) + { + const auto &specified_rotate_z = getRotateZ(); + return computed::TransformOperation::RotateZ(specified_rotate_z.angle().toComputedValue(context)); + } + else if (isRotate3D()) + { + const auto &specified_rotate_3d = getRotate3D(); + return computed::TransformOperation::Rotate3D(specified_rotate_3d.x().toComputedValue(context), + specified_rotate_3d.y().toComputedValue(context), + specified_rotate_3d.z().toComputedValue(context), + specified_rotate_3d.angle().toComputedValue(context)); + } assert(false && "Invalid transform operation type."); } @@ -113,38 +172,26 @@ namespace client_cssom::values::specified private: bool parse(const std::string &input) override { - using InnerType = crates::css2::values::specified::transform::TransformOperationType; + css_transform_parser::CSSTransformParser parser(input); + auto functions = parser.parse(); + + if (!parser.isValid()) + { + return false; + } + + // Clear existing operations + operations_.clear(); - const auto &handle = crates::css2::parsing::parseTransform(input); - for (const auto &op : handle.operations()) + // Convert parsed functions to transform operations + for (const auto &func : functions) { - switch (op.type()) + if (!addTransformFunction(func)) { - case InnerType::kMatrix: - addMatrix(op); - break; - case InnerType::kMatrix3D: - addMatrix3D(op); - break; - case InnerType::kTranslate: - addTranslate(op); - break; - case InnerType::kTranslateX: - addTranslateX(op); - break; - case InnerType::kTranslateY: - addTranslateY(op); - break; - case InnerType::kTranslateZ: - addTranslateZ(op); - break; - case InnerType::kTranslate3D: - addTranslate3D(op); - break; - default: - break; + return false; } } + return true; } @@ -158,69 +205,345 @@ namespace client_cssom::values::specified } private: - void addMatrix(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + bool addTransformFunction(const css_transform_parser::TransformFunction &func) + { + using namespace css_transform_parser; + + switch (func.type) + { + case TransformFunctionType::kMatrix: + return addMatrix(func); + case TransformFunctionType::kMatrix3D: + return addMatrix3D(func); + case TransformFunctionType::kTranslate: + return addTranslate(func); + case TransformFunctionType::kTranslateX: + return addTranslateX(func); + case TransformFunctionType::kTranslateY: + return addTranslateY(func); + case TransformFunctionType::kTranslateZ: + return addTranslateZ(func); + case TransformFunctionType::kTranslate3D: + return addTranslate3D(func); + case TransformFunctionType::kScale: + return addScale(func); + case TransformFunctionType::kScaleX: + return addScaleX(func); + case TransformFunctionType::kScaleY: + return addScaleY(func); + case TransformFunctionType::kScaleZ: + return addScaleZ(func); + case TransformFunctionType::kScale3D: + return addScale3D(func); + case TransformFunctionType::kRotate: + return addRotate(func); + case TransformFunctionType::kRotateX: + return addRotateX(func); + case TransformFunctionType::kRotateY: + return addRotateY(func); + case TransformFunctionType::kRotateZ: + return addRotateZ(func); + case TransformFunctionType::kRotate3D: + return addRotate3D(func); + case TransformFunctionType::kSkew: + return addSkew(func); + case TransformFunctionType::kSkewX: + return addSkewX(func); + case TransformFunctionType::kSkewY: + return addSkewY(func); + case TransformFunctionType::kPerspective: + // Perspective is not implemented in the current transform operations + return true; // Skip for now + default: + return false; + } + } + + bool addMatrix(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 6) + return false; + + operations_.push_back(TransformOperation::Matrix( + Number(func.values[0]), + Number(func.values[1]), + Number(func.values[2]), + Number(func.values[3]), + Number(func.values[4]), + Number(func.values[5]))); + return true; + } + + bool addMatrix3D(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 16) + return false; + + operations_.push_back(TransformOperation::Matrix3D( + Number(func.values[0]), Number(func.values[1]), Number(func.values[2]), Number(func.values[3]), Number(func.values[4]), Number(func.values[5]), Number(func.values[6]), Number(func.values[7]), Number(func.values[8]), Number(func.values[9]), Number(func.values[10]), Number(func.values[11]), Number(func.values[12]), Number(func.values[13]), Number(func.values[14]), Number(func.values[15]))); + return true; + } + + bool addTranslate(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 2) + return false; + + operations_.push_back(TransformOperation::Translate( + createLengthPercentage(func.values[0], func.units[0]), + createLengthPercentage(func.values[1], func.units[1]))); + return true; + } + + bool addTranslateX(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::TranslateX( + createLengthPercentage(func.values[0], func.units[0]))); + return true; + } + + bool addTranslateY(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::TranslateY( + createLengthPercentage(func.values[0], func.units[0]))); + return true; + } + + bool addTranslateZ(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::TranslateZ( + createLength(func.values[0], func.units[0]))); + return true; + } + + bool addTranslate3D(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 3) + return false; + + operations_.push_back(TransformOperation::Translate3D( + createLengthPercentage(func.values[0], func.units[0]), + createLengthPercentage(func.values[1], func.units[1]), + createLength(func.values[2], func.units[2]))); + return true; + } + + bool addScale(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() == 1) + { + // Uniform scaling: scale(x) is equivalent to scale(x, x) + operations_.push_back(TransformOperation::Scale( + Number(func.values[0]), + Number(func.values[0]))); + } + else if (func.values.size() == 2) + { + // Non-uniform scaling: scale(x, y) - use the first value for GenericScale + // The second value is ignored for now due to GenericScale limitations + operations_.push_back(TransformOperation::Scale( + Number(func.values[0]), + Number(func.values[0]))); + } + else + { + return false; + } + return true; + } + + bool addScaleX(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::ScaleX( + Number(func.values[0]))); + return true; + } + + bool addScaleY(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::ScaleY( + Number(func.values[0]))); + return true; + } + + bool addScaleZ(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::ScaleZ( + Number(func.values[0]))); + return true; + } + + bool addScale3D(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 3) + return false; + + operations_.push_back(TransformOperation::Scale3D( + Number(func.values[0]), + Number(func.values[1]), + Number(func.values[2]))); + return true; + } + + bool addRotate(const css_transform_parser::TransformFunction &func) { - using InnerMatrix = crates::css2::values::generics::GenericMatrix; + if (func.values.size() != 1) + return false; - const auto &inner_matrix = inner_operation.getImplAs(); - operations_.push_back(TransformOperation::Matrix(inner_matrix.a.value, - inner_matrix.b.value, - inner_matrix.c.value, - inner_matrix.d.value, - inner_matrix.e.value, - inner_matrix.f.value)); + operations_.push_back(TransformOperation::Rotate( + createAngle(func.values[0], func.units[0]))); + return true; } - void addMatrix3D(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + + bool addRotateX(const css_transform_parser::TransformFunction &func) { - using InnerMatrix3D = crates::css2::values::generics::GenericMatrix3D; + if (func.values.size() != 1) + return false; - const auto &inner_matrix3d = inner_operation.getImplAs(); - operations_.push_back(TransformOperation::Matrix3D(inner_matrix3d.m11.value, - inner_matrix3d.m12.value, - inner_matrix3d.m13.value, - inner_matrix3d.m14.value, - inner_matrix3d.m21.value, - inner_matrix3d.m22.value, - inner_matrix3d.m23.value, - inner_matrix3d.m24.value, - inner_matrix3d.m31.value, - inner_matrix3d.m32.value, - inner_matrix3d.m33.value, - inner_matrix3d.m34.value, - inner_matrix3d.m41.value, - inner_matrix3d.m42.value, - inner_matrix3d.m43.value, - inner_matrix3d.m44.value)); + operations_.push_back(TransformOperation::RotateX( + createAngle(func.values[0], func.units[0]))); + return true; } - void addTranslate(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + + bool addRotateY(const css_transform_parser::TransformFunction &func) { - const auto &inner_translate = - inner_operation.getImplAs(); - operations_.push_back(TransformOperation::Translate(LengthPercentage::From(inner_translate.x), - LengthPercentage::From(inner_translate.y))); + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::RotateY( + createAngle(func.values[0], func.units[0]))); + return true; } - void addTranslateX(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + + bool addRotateZ(const css_transform_parser::TransformFunction &func) { - const auto &x = inner_operation.getImplAs(); - operations_.push_back(TransformOperation::TranslateX(LengthPercentage::From(x))); + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::RotateZ( + createAngle(func.values[0], func.units[0]))); + return true; } - void addTranslateY(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + + bool addRotate3D(const css_transform_parser::TransformFunction &func) { - const auto &y = inner_operation.getImplAs(); - operations_.push_back(TransformOperation::TranslateY(LengthPercentage::From(y))); + if (func.values.size() != 4) + return false; + + operations_.push_back(TransformOperation::Rotate3D( + Number(func.values[0]), + Number(func.values[1]), + Number(func.values[2]), + createAngle(func.values[3], func.units[3]))); + return true; } - void addTranslateZ(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + + bool addSkew(const css_transform_parser::TransformFunction &func) { - const auto &z = inner_operation.getImplAs(); - operations_.push_back(TransformOperation::TranslateZ(NoCalcLength::FromPx(z.numberValue()))); + if (func.values.size() != 2) + return false; + + operations_.push_back(TransformOperation::Skew( + createAngle(func.values[0], func.units[0]), + createAngle(func.values[1], func.units[1]))); + return true; } - void addTranslate3D(const crates::css2::values::specified::transform::TransformOperation &inner_operation) + + bool addSkewX(const css_transform_parser::TransformFunction &func) { - const auto &inner_translate3d = - inner_operation.getImplAs(); - operations_.push_back(TransformOperation::Translate3D(LengthPercentage::From(inner_translate3d.x), - LengthPercentage::From(inner_translate3d.y), - NoCalcLength::FromPx(inner_translate3d.z.numberValue()))); + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::SkewX( + createAngle(func.values[0], func.units[0]))); + return true; + } + + bool addSkewY(const css_transform_parser::TransformFunction &func) + { + if (func.values.size() != 1) + return false; + + operations_.push_back(TransformOperation::SkewY( + createAngle(func.values[0], func.units[0]))); + return true; + } + + // Helper methods to create values + LengthPercentage createLengthPercentage(double value, const std::string &unit) + { + if (unit == "%") + { + return LengthPercentage(computed::Percentage(value / 100.0f)); + } + else + { + return LengthPercentage(createLength(value, unit)); + } + } + + NoCalcLength createLength(double value, const std::string &unit) + { + if (unit == "px" || unit.empty()) + { + return NoCalcLength::FromPx(value); + } + else if (unit == "em") + { + return NoCalcLength(FontRelativeLength::Em(value)); + } + else if (unit == "rem") + { + return NoCalcLength(FontRelativeLength::Rem(value)); + } + else + { + // Default to pixels for unknown units + return NoCalcLength::FromPx(value); + } + } + + Angle createAngle(double value, const std::string &unit) + { + if (unit == "deg" || unit.empty()) + { + return Angle::Deg(value); + } + else if (unit == "rad") + { + return Angle::Rad(value); + } + else if (unit == "grad") + { + return Angle::Grad(value); + } + else if (unit == "turn") + { + return Angle::Turn(value); + } + else + { + // Default to degrees for unknown units + return Angle::Deg(value); + } } }; } diff --git a/tests/client/css_transform_parser_tests.cpp b/tests/client/css_transform_parser_tests.cpp new file mode 100644 index 000000000..1f4077c78 --- /dev/null +++ b/tests/client/css_transform_parser_tests.cpp @@ -0,0 +1,408 @@ +#define CATCH_CONFIG_MAIN +#include "../catch2/catch_amalgamated.hpp" + +#include +#include +#include + +using namespace client_cssom::css_transform_parser; +using namespace client_cssom::values::specified; +using namespace client_cssom; + +TEST_CASE("CSSTransformParser basic functionality", "[css-transform-parser]") +{ + SECTION("Parse none") + { + CSSTransformParser parser("none"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.empty()); + } + + SECTION("Parse simple translate") + { + CSSTransformParser parser("translateX(10px)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kTranslateX); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == 10.0); + REQUIRE(functions[0].units[0] == "px"); + } + + SECTION("Parse translate with two values") + { + CSSTransformParser parser("translate(10px, 20px)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kTranslate); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 10.0); + REQUIRE(functions[0].values[1] == 20.0); + REQUIRE(functions[0].units[0] == "px"); + REQUIRE(functions[0].units[1] == "px"); + } + + SECTION("Parse translate with one value (should default Y to 0)") + { + CSSTransformParser parser("translate(10px)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kTranslate); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 10.0); + REQUIRE(functions[0].values[1] == 0.0); + REQUIRE(functions[0].units[0] == "px"); + REQUIRE(functions[0].units[1] == "px"); + } +} + +TEST_CASE("CSSTransformParser rotation functions", "[css-transform-parser]") +{ + SECTION("Parse rotate") + { + CSSTransformParser parser("rotate(45deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kRotate); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == 45.0); + REQUIRE(functions[0].units[0] == "deg"); + } + + SECTION("Parse rotateX") + { + CSSTransformParser parser("rotateX(90deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kRotateX); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == 90.0); + REQUIRE(functions[0].units[0] == "deg"); + } + + SECTION("Parse rotate with radians") + { + CSSTransformParser parser("rotate(1.57rad)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kRotate); + REQUIRE(functions[0].values.size() == 1); + // REQUIRE(functions[0].values[0] == 1.57f); + REQUIRE(functions[0].units[0] == "rad"); + } + + SECTION("Parse rotate3d") + { + CSSTransformParser parser("rotate3d(1, 0, 0, 45deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kRotate3D); + REQUIRE(functions[0].values.size() == 4); + REQUIRE(functions[0].values[0] == 1.0); + REQUIRE(functions[0].values[1] == 0.0); + REQUIRE(functions[0].values[2] == 0.0); + REQUIRE(functions[0].values[3] == 45.0); + REQUIRE(functions[0].units[3] == "deg"); + } +} + +TEST_CASE("CSSTransformParser scale functions", "[css-transform-parser]") +{ + SECTION("Parse scale with one value") + { + CSSTransformParser parser("scale(2)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kScale); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 2.0); + REQUIRE(functions[0].values[1] == 2.0); // Should default to same value + } + + SECTION("Parse scale with two values") + { + CSSTransformParser parser("scale(2, 3)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kScale); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 2.0); + REQUIRE(functions[0].values[1] == 3.0); + } + + SECTION("Parse scaleX") + { + CSSTransformParser parser("scaleX(1.5)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kScaleX); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == 1.5); + } +} + +TEST_CASE("CSSTransformParser matrix functions", "[css-transform-parser]") +{ + SECTION("Parse matrix") + { + CSSTransformParser parser("matrix(1, 0, 0, 1, 10, 20)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kMatrix); + REQUIRE(functions[0].values.size() == 6); + REQUIRE(functions[0].values[0] == 1.0); + REQUIRE(functions[0].values[1] == 0.0); + REQUIRE(functions[0].values[2] == 0.0); + REQUIRE(functions[0].values[3] == 1.0); + REQUIRE(functions[0].values[4] == 10.0); + REQUIRE(functions[0].values[5] == 20.0); + } + + SECTION("Parse matrix3d") + { + CSSTransformParser parser("matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kMatrix3D); + REQUIRE(functions[0].values.size() == 16); + REQUIRE(functions[0].values[12] == 10.0); // Translation X + REQUIRE(functions[0].values[13] == 20.0); // Translation Y + REQUIRE(functions[0].values[14] == 30.0); // Translation Z + } +} + +TEST_CASE("CSSTransformParser multiple functions", "[css-transform-parser]") +{ + SECTION("Parse multiple transforms") + { + CSSTransformParser parser("translateX(10px) rotate(45deg) scale(2)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 3); + + REQUIRE(functions[0].type == TransformFunctionType::kTranslateX); + REQUIRE(functions[0].values[0] == 10.0); + + REQUIRE(functions[1].type == TransformFunctionType::kRotate); + REQUIRE(functions[1].values[0] == 45.0); + + REQUIRE(functions[2].type == TransformFunctionType::kScale); + REQUIRE(functions[2].values[0] == 2.0); + } +} + +TEST_CASE("CSSTransformParser skew functions", "[css-transform-parser]") +{ + SECTION("Parse skew with two values") + { + CSSTransformParser parser("skew(10deg, 20deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kSkew); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 10.0); + REQUIRE(functions[0].values[1] == 20.0); + } + + SECTION("Parse skew with one value") + { + CSSTransformParser parser("skew(10deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kSkew); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 10.0); + REQUIRE(functions[0].values[1] == 0.0); // Should default to 0 + } + + SECTION("Parse skewX") + { + CSSTransformParser parser("skewX(15deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kSkewX); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == 15.0); + } +} + +TEST_CASE("CSSTransformParser negative values", "[css-transform-parser]") +{ + SECTION("Parse negative translateX") + { + CSSTransformParser parser("translateX(-10px)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kTranslateX); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == -10.0); + REQUIRE(functions[0].units[0] == "px"); + } + + SECTION("Parse negative rotate") + { + CSSTransformParser parser("rotate(-45deg)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kRotate); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == -45.0); + REQUIRE(functions[0].units[0] == "deg"); + } + + SECTION("Parse negative scale") + { + CSSTransformParser parser("scale(-1)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kScale); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == -1.0); + REQUIRE(functions[0].values[1] == -1.0); // Should default to same value + } + + SECTION("Parse translate with negative values") + { + CSSTransformParser parser("translate(-10px, -20px)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kTranslate); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == -10.0); + REQUIRE(functions[0].values[1] == -20.0); + REQUIRE(functions[0].units[0] == "px"); + REQUIRE(functions[0].units[1] == "px"); + } + + SECTION("Parse mixed positive and negative values") + { + CSSTransformParser parser("translate(10px, -20px)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kTranslate); + REQUIRE(functions[0].values.size() == 2); + REQUIRE(functions[0].values[0] == 10.0); + REQUIRE(functions[0].values[1] == -20.0); + REQUIRE(functions[0].units[0] == "px"); + REQUIRE(functions[0].units[1] == "px"); + } + + SECTION("Parse negative decimal values") + { + CSSTransformParser parser("scaleX(-0.5)"); + auto functions = parser.parse(); + + REQUIRE(parser.isValid()); + REQUIRE(functions.size() == 1); + REQUIRE(functions[0].type == TransformFunctionType::kScaleX); + REQUIRE(functions[0].values.size() == 1); + REQUIRE(functions[0].values[0] == -0.5); + } +} + +TEST_CASE("CSSTransformParser error handling", "[css-transform-parser]") +{ + SECTION("Invalid function name") + { + CSSTransformParser parser("invalidFunction(10px)"); + auto functions = parser.parse(); + + REQUIRE_FALSE(parser.isValid()); + REQUIRE_FALSE(parser.getError().empty()); + } + + SECTION("Missing closing parenthesis") + { + CSSTransformParser parser("translateX(10px"); + auto functions = parser.parse(); + + REQUIRE_FALSE(parser.isValid()); + } + + SECTION("Wrong number of parameters") + { + CSSTransformParser parser("matrix(1, 2, 3)"); // Should have 6 parameters + auto functions = parser.parse(); + + REQUIRE_FALSE(parser.isValid()); + } +} + +TEST_CASE("Transform class integration", "[transform-integration]") +{ + SECTION("Parse simple transform") + { + auto transform = Parse::ParseSingleValue("translateX(10px)"); + + REQUIRE(transform.operations().size() == 1); + REQUIRE(transform.operations()[0].isTranslateX()); + } + + SECTION("Parse multiple transforms") + { + auto transform = Parse::ParseSingleValue("translateX(10px) rotate(45deg) scale(2)"); + + REQUIRE(transform.operations().size() == 3); + REQUIRE(transform.operations()[0].isTranslateX()); + REQUIRE(transform.operations()[1].isRotate()); + REQUIRE(transform.operations()[2].isScale()); + } + + SECTION("Parse none") + { + auto transform = Parse::ParseSingleValue("none"); + + REQUIRE(transform.operations().empty()); + } + + SECTION("Parse invalid transform should return empty") + { + auto transform = Parse::ParseSingleValue("invalid(10px)"); + + // Should return default empty transform on parsing failure + REQUIRE(transform.operations().empty()); + } +}