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());
+ }
+}