From 0de33c574d33e26684a872fc047fea0cb6f120c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 3 Aug 2026 22:52:25 -0300 Subject: [PATCH] Fix CSS statement at-rules consuming following rules - stop statement at-rules such as @charset at their semicolon - preserve the following selector and declaration block - support feature-level not() expressions in media queries - add regression coverage for @charset and negated media features Fixes Lobsters theme variables being discarded, which caused mixed light and dark colors when loading its stylesheets. --- include/eepp/ui/css/mediaquery.hpp | 1 + src/eepp/ui/css/mediaquery.cpp | 13 ++++++- src/eepp/ui/css/stylesheetparser.cpp | 12 +++++- .../unit_tests/uicss_inheritance_tests.cpp | 39 +++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/include/eepp/ui/css/mediaquery.hpp b/include/eepp/ui/css/mediaquery.hpp index 1aa249430..61d407ec4 100644 --- a/include/eepp/ui/css/mediaquery.hpp +++ b/include/eepp/ui/css/mediaquery.hpp @@ -119,6 +119,7 @@ struct EE_API MediaQueryExpression { float fval{ 0 }; float fval2{ 0 }; bool checkAsBool{ false }; + bool negated{ false }; std::string valStr; MediaQueryExpression(); diff --git a/src/eepp/ui/css/mediaquery.cpp b/src/eepp/ui/css/mediaquery.cpp index 20969c652..0627d74a0 100644 --- a/src/eepp/ui/css/mediaquery.cpp +++ b/src/eepp/ui/css/mediaquery.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -50,8 +51,18 @@ MediaQuery::ptr MediaQuery::parse( const std::string& str ) { if ( tok.at( tok.length() - 1 ) == ')' ) { tok.erase( tok.length() - 1, 1 ); } + String::trimInPlace( tok ); MediaQueryExpression expr; + if ( String::startsWith( tok, "not" ) && + ( tok.length() == 3 || std::isspace( static_cast( tok[3] ) ) || + tok[3] == '(' ) ) { + expr.negated = true; + tok.erase( 0, 3 ); + String::trimInPlace( tok ); + if ( tok.length() >= 2 && tok.front() == '(' && tok.back() == ')' ) + tok = tok.substr( 1, tok.length() - 2 ); + } std::vector exprTokens = String::split( tok, ':' ); if ( !exprTokens.empty() ) { String::trimInPlace( exprTokens[0] ); @@ -119,7 +130,7 @@ bool MediaQuery::check( const MediaFeatures& features ) const { res = true; for ( auto& expr : mExpressions ) { - if ( !expr.check( features ) ) { + if ( expr.check( features ) == expr.negated ) { res = false; break; } diff --git a/src/eepp/ui/css/stylesheetparser.cpp b/src/eepp/ui/css/stylesheetparser.cpp index fac9ac473..7a39c5ba0 100644 --- a/src/eepp/ui/css/stylesheetparser.cpp +++ b/src/eepp/ui/css/stylesheetparser.cpp @@ -186,8 +186,16 @@ int StyleSheetParser::readSelector( const std::string& css, ReadState& rs, std:: if ( css[pos] != '\n' && css[pos] != '\r' && css[pos] != '\t' ) buffer += css[pos]; - if ( css[pos] == ';' && String::startsWith( buffer, "@import" ) ) { - return initialPos; + if ( css[pos] == ';' ) { + std::string_view statement = String::trim( std::string_view{ buffer } ); + if ( String::startsWith( statement, "@import" ) ) + return initialPos; + if ( !statement.empty() && statement.front() == '@' ) { + // Statement at-rules (for example @charset) end here. Do not let them become + // part of the selector preceding the next block. + buffer.clear(); + initialPos = pos + 1; + } } pos++; diff --git a/src/tests/unit_tests/uicss_inheritance_tests.cpp b/src/tests/unit_tests/uicss_inheritance_tests.cpp index de5f5e815..4f6bb8afb 100644 --- a/src/tests/unit_tests/uicss_inheritance_tests.cpp +++ b/src/tests/unit_tests/uicss_inheritance_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,18 @@ UTEST( CSSParser, CommentsPreserveDeclarationContext ) { EXPECT_TRUE( width->second.getValue() == "140px" ); } +UTEST( CSSParser, StatementAtRuleDoesNotConsumeFollowingRule ) { + StyleSheetParser parser; + ASSERT_TRUE( parser.loadFromString( std::string_view{ R"css( + @charset "UTF-8"; + :root { --color-bg: #0c0c0c; } + )css" } ) ); + + auto rootStyle = parser.getStyleSheet().getStyleFromSelector( ":root" ); + ASSERT_TRUE( rootStyle != nullptr ); + EXPECT_TRUE( rootStyle->getVariableByName( "--color-bg" ).getValue() == "#0c0c0c" ); +} + UTEST( CSSInheritance, HtmlXmlLoadingInheritance ) { UIApplication app( WindowSettings( 800, 600, "eepp - CSS Inheritance Test", WindowStyle::Default, @@ -1266,6 +1279,32 @@ UTEST( CSSVariables, VarInRgbViaIntermediateVar ) { EXPECT_EQ( 191, result.a ); // 0.75 * 255 = 191 } +UTEST( CSSMediaQuery, NegatedFeatureExpression ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - Negated Media Feature Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + MediaFeatures features; + features.prefersColorScheme = "dark"; + features.prefersContrast = "no-preference"; + auto darkNormal = + MediaQuery::parse( "(prefers-color-scheme: dark) and (not (prefers-contrast: more))" ); + ASSERT_TRUE( darkNormal != nullptr ); + EXPECT_TRUE( darkNormal->check( features ) ); + + features.prefersContrast = "more"; + EXPECT_FALSE( darkNormal->check( features ) ); + + auto lightHigh = + MediaQuery::parse( "(not (prefers-color-scheme: dark)) and (prefers-contrast: more)" ); + ASSERT_TRUE( lightHigh != nullptr ); + features.prefersColorScheme = "light"; + EXPECT_TRUE( lightHigh->check( features ) ); + features.prefersColorScheme = "dark"; + EXPECT_FALSE( lightHigh->check( features ) ); +} + UTEST( CSSVariables, LightDarkBasic ) { // Light scheme: picks first parameter of light-dark() UIApplication lightApp(