// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2025 Kacper Donat * * * * This file is part of FreeCAD. * * * * FreeCAD is free software: you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 2.1 of the * * License, or (at your option) any later version. * * * * FreeCAD is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with FreeCAD. If not, see * * . * * * ***************************************************************************/ #include "Parser.h" #include "ParameterManager.h" #include #include #include #include #include #include #include namespace Gui::StyleParameters { Value ParameterReference::evaluate(const EvaluationContext& context) const { return context.manager->resolve(name, context.context).value_or("@" + name); } Value Number::evaluate([[maybe_unused]] const EvaluationContext& context) const { return value; } Value Color::evaluate([[maybe_unused]] const EvaluationContext& context) const { return color; } Value FunctionCall::evaluate(const EvaluationContext& context) const { const auto lightenOrDarken = [this](const EvaluationContext& context) -> Value { if (arguments.size() != 2) { THROWM( Base::ExpressionError, fmt::format("Function '{}' expects 2 arguments, got {}", functionName, arguments.size()) ); } auto colorArg = arguments[0]->evaluate(context); auto amountArg = arguments[1]->evaluate(context); if (!std::holds_alternative(colorArg)) { THROWM(Base::ExpressionError, fmt::format("'{}' is not supported for colors", functionName)); } auto color = std::get(colorArg).asValue(); // In Qt if you want to make color 20% darker or lighter, you need to pass 120 as the value // we, however, want users to pass only the relative difference, hence we need to add the // 100 required by Qt. // // NOLINTNEXTLINE(*-magic-numbers) auto amount = 100 + static_cast(std::get(amountArg).value); if (functionName == "lighten") { return Base::Color::fromValue(color.lighter(amount)); } if (functionName == "darken") { return Base::Color::fromValue(color.darker(amount)); } return {}; }; const auto blend = [this](const EvaluationContext& context) -> Value { if (arguments.size() != 3) { THROWM( Base::ExpressionError, fmt::format("Function '{}' expects 3 arguments, got {}", functionName, arguments.size()) ); } auto firstColorArg = arguments[0]->evaluate(context); auto secondColorArg = arguments[1]->evaluate(context); auto amountArg = arguments[2]->evaluate(context); if (!std::holds_alternative(firstColorArg)) { THROWM( Base::ExpressionError, fmt::format("first argument of '{}' must be color", functionName) ); } if (!std::holds_alternative(secondColorArg)) { THROWM( Base::ExpressionError, fmt::format("second argument of '{}' must be color", functionName) ); } auto firstColor = std::get(firstColorArg); auto secondColor = std::get(secondColorArg); auto amount = Base::fromPercent(static_cast(std::get(amountArg).value)); return Base::Color( (1 - amount) * firstColor.r + amount * secondColor.r, (1 - amount) * firstColor.g + amount * secondColor.g, (1 - amount) * firstColor.b + amount * secondColor.b ); }; std::map> functions = { {"lighten", lightenOrDarken}, {"darken", lightenOrDarken}, {"blend", blend}, }; if (functions.contains(functionName)) { auto function = functions.at(functionName); return function(context); } THROWM(Base::ExpressionError, fmt::format("Unknown function '{}'", functionName)); } Value BinaryOp::evaluate(const EvaluationContext& context) const { Value lval = left->evaluate(context); Value rval = right->evaluate(context); if (!std::holds_alternative(lval) || !std::holds_alternative(rval)) { THROWM(Base::ExpressionError, "Math operations are supported only on lengths"); } auto lhs = std::get(lval); auto rhs = std::get(rval); switch (op) { case Operator::Add: return lhs + rhs; case Operator::Subtract: return lhs - rhs; case Operator::Multiply: return lhs * rhs; case Operator::Divide: return lhs / rhs; default: THROWM(Base::ExpressionError, "Unknown operator"); } } Value UnaryOp::evaluate(const EvaluationContext& context) const { Value val = operand->evaluate(context); if (std::holds_alternative(val)) { THROWM(Base::ExpressionError, "Unary operations on colors are not supported"); } auto v = std::get(val); switch (op) { case Operator::Add: return v; case Operator::Subtract: return -v; default: THROWM(Base::ExpressionError, "Unknown unary operator"); } } std::unique_ptr Parser::parse() { auto expr = parseExpression(); skipWhitespace(); if (pos != input.size()) { THROWM( Base::ParserError, fmt::format("Unexpected characters at end of input: {}", input.substr(pos)) ); } return expr; } bool Parser::peekString(const char* function) const { return input.compare(pos, strlen(function), function) == 0; } std::unique_ptr Parser::parseExpression() { auto expr = parseTerm(); while (true) { skipWhitespace(); if (match('+')) { expr = std::make_unique(std::move(expr), Operator::Add, parseTerm()); } else if (match('-')) { expr = std::make_unique(std::move(expr), Operator::Subtract, parseTerm()); } else { break; } } return expr; } std::unique_ptr Parser::parseTerm() { auto expr = parseFactor(); while (true) { skipWhitespace(); if (match('*')) { expr = std::make_unique(std::move(expr), Operator::Multiply, parseFactor()); } else if (match('/')) { expr = std::make_unique(std::move(expr), Operator::Divide, parseFactor()); } else { break; } } return expr; } std::unique_ptr Parser::parseFactor() { skipWhitespace(); if (match('+') || match('-')) { Operator op = (input[pos - 1] == '+') ? Operator::Add : Operator::Subtract; return std::make_unique(op, parseFactor()); } if (match('(')) { auto expr = parseExpression(); if (!match(')')) { THROWM(Base::ParserError, fmt::format("Expected ')', got '{}'", input[pos])); } return expr; } if (peekColor()) { return parseColor(); } if (peekParameter()) { return parseParameter(); } if (peekFunction()) { return parseFunctionCall(); } return parseNumber(); } bool Parser::peekColor() { skipWhitespace(); // clang-format off return input[pos] == '#' || peekString(rgbFunction) || peekString(rgbaFunction); // clang-format on } std::unique_ptr Parser::parseColor() { const auto parseHexadecimalColor = [&]() { constexpr int hexadecimalBase = 16; // Format is #RRGGBB pos++; int r = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); pos += 2; int g = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); pos += 2; int b = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); pos += 2; return std::make_unique(Base::Color(r / 255.0, g / 255.0, b / 255.0)); }; const auto parseFunctionStyleColor = [&]() { bool hasAlpha = peekString(rgbaFunction); pos += hasAlpha ? strlen(rgbaFunction) : strlen(rgbFunction); int r = parseInt(); if (!match(',')) { THROWM(Base::ParserError, fmt::format("Expected ',' after red, got '{}'", input[pos])); } int g = parseInt(); if (!match(',')) { THROWM(Base::ParserError, fmt::format("Expected ',' after green, got '{}'", input[pos])); } int b = parseInt(); int a = 255; // NOLINT(*-magic-numbers) if (hasAlpha) { if (!match(',')) { THROWM(Base::ParserError, fmt::format("Expected ',' after blue, got '{}'", input[pos])); } a = parseInt(); } if (!match(')')) { THROWM( Base::ParserError, fmt::format("Expected ')' after color arguments, got '{}'", input[pos]) ); } return std::make_unique(Base::Color(r / 255.0, g / 255.0, b / 255.0, a / 255.0)); }; skipWhitespace(); try { if (input[pos] == '#') { return parseHexadecimalColor(); } if (peekString(rgbFunction) || peekString(rgbaFunction)) { return parseFunctionStyleColor(); } } catch (std::invalid_argument&) { THROWM( Base::ParserError, "Invalid color format, expected #RRGGBB or rgb(r,g,b) or rgba(r,g,b,a)" ); } THROWM(Base::ParserError, "Unknown color format"); } bool Parser::peekParameter() { skipWhitespace(); return pos < input.size() && input[pos] == '@'; } std::unique_ptr Parser::parseParameter() { skipWhitespace(); if (!match('@')) { THROWM(Base::ParserError, fmt::format("Expected '@' for parameter, got '{}'", input[pos])); } size_t start = pos; while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_')) { ++pos; } if (start == pos) { THROWM( Base::ParserError, fmt::format("Expected parameter name after '@', got '{}'", input[pos]) ); } return std::make_unique(input.substr(start, pos - start)); } bool Parser::peekFunction() { skipWhitespace(); return pos < input.size() && isalpha(input[pos]); } std::unique_ptr Parser::parseFunctionCall() { skipWhitespace(); size_t start = pos; while (pos < input.size() && isalnum(input[pos])) { ++pos; } std::string functionName = input.substr(start, pos - start); if (!match('(')) { THROWM(Base::ParserError, fmt::format("Expected '(' after function name, got '{}'", input[pos])); } std::vector> arguments; if (!match(')')) { do { // NOLINT(*-avoid-do-while) arguments.push_back(parseExpression()); } while (match(',')); if (!match(')')) { THROWM( Base::ParserError, fmt::format("Expected ')' after function arguments, got '{}'", input[pos]) ); } } return std::make_unique(functionName, std::move(arguments)); } int Parser::parseInt() { skipWhitespace(); size_t start = pos; while (pos < input.size() && (isdigit(input[pos]) || input[pos] == '.')) { ++pos; } return std::stoi(input.substr(start, pos - start)); } std::unique_ptr Parser::parseNumber() { skipWhitespace(); size_t start = pos; while (pos < input.size() && (isdigit(input[pos]) || input[pos] == '.')) { ++pos; } std::string number = input.substr(start, pos - start); try { double value = std::stod(number); std::string unit = parseUnit(); return std::make_unique(value, unit); } catch (std::invalid_argument&) { THROWM(Base::ParserError, fmt::format("Invalid number: {}", number)); } } std::string Parser::parseUnit() { skipWhitespace(); size_t start = pos; while (pos < input.size() && (isalpha(input[pos]) || input[pos] == '%')) { ++pos; } if (start == pos) { return ""; } return input.substr(start, pos - start); } bool Parser::match(char expected) { skipWhitespace(); if (pos < input.size() && input[pos] == expected) { ++pos; return true; } return false; } void Parser::skipWhitespace() { while (pos < input.size() && isspace(input[pos])) { ++pos; } } } // namespace Gui::StyleParameters