From 207340b60d2b26a5372fe0196fac5a98832e8bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 4 Sep 2026 20:57:55 -0400 Subject: [PATCH 1/2] Fixed case-sensitive matching of CSS keywords in parsers The @supports condition parser compared the "and" / "or" keywords with ordinal equality, so an uppercase or mixed-case keyword silently discarded the whole conditional group and every rule inside it. The keyframe selector parser did the same for "from" / "to", leaving the rule in the CSSOM with a null key. Switched both to the case-insensitive Isi helper, including the chain continuation in Scan, which compared each subsequent keyword against the raw text of the first one. --- .../Rules/CssKeyframeRule.cs | 33 ++++++++++++ src/AngleSharp.Css.Tests/Rules/CssSupports.cs | 52 +++++++++++++++++++ .../Parser/Micro/ConditionParser.cs | 6 +-- .../Parser/Micro/KeyframeParser.cs | 4 +- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/AngleSharp.Css.Tests/Rules/CssKeyframeRule.cs b/src/AngleSharp.Css.Tests/Rules/CssKeyframeRule.cs index efcc9a55..9fc71c79 100644 --- a/src/AngleSharp.Css.Tests/Rules/CssKeyframeRule.cs +++ b/src/AngleSharp.Css.Tests/Rules/CssKeyframeRule.cs @@ -84,5 +84,38 @@ public void KeyframeRuleWithPercentage_Issue128() Assert.AreEqual(3, rule.Key.Stops.Count()); Assert.AreEqual(0, rule.Style.Length); } + + [Test] + public void KeyframeRuleWithUppercaseFrom() + { + var rule = ParseKeyframeRule(@" FROM { + margin-left: 0px; + }"); + Assert.IsNotNull(rule); + Assert.AreEqual("0%", rule.KeyText); + Assert.AreEqual(1, rule.Key.Stops.Count()); + Assert.AreEqual(1, rule.Style.Length); + } + + [Test] + public void KeyframeRuleWithUppercaseTo() + { + var rule = ParseKeyframeRule(@" TO { + margin-left: 200px; + }"); + Assert.IsNotNull(rule); + Assert.AreEqual("100%", rule.KeyText); + Assert.AreEqual(1, rule.Key.Stops.Count()); + Assert.AreEqual(1, rule.Style.Length); + } + + [Test] + public void KeyframeRuleWithMixedCaseFromAndTo() + { + var rule = ParseKeyframeRule(@" From, To { }"); + Assert.IsNotNull(rule); + Assert.AreEqual("0%, 100%", rule.KeyText); + Assert.AreEqual(2, rule.Key.Stops.Count()); + } } } diff --git a/src/AngleSharp.Css.Tests/Rules/CssSupports.cs b/src/AngleSharp.Css.Tests/Rules/CssSupports.cs index 1a512803..c419dd13 100644 --- a/src/AngleSharp.Css.Tests/Rules/CssSupports.cs +++ b/src/AngleSharp.Css.Tests/Rules/CssSupports.cs @@ -209,5 +209,57 @@ public void SupportsNegatedDisplayFlexRuleWithDeclarations() Assert.AreEqual("not (display: flex)", supports.ConditionText); Assert.IsFalse(supports.Condition.Check(device)); } + + [Test] + public void SupportsUppercaseAndKeywordRule() + { + var source = @"@supports ((background-color: red) AND (color: blue)) { }"; + var sheet = ParseStyleSheet(source); + var device = new DefaultRenderDevice(); + Assert.AreEqual(1, sheet.Rules.Length); + Assert.IsInstanceOf(sheet.Rules[0]); + var supports = sheet.Rules[0] as CssSupportsRule; + Assert.AreEqual("((background-color: red) and (color: blue))", supports.ConditionText); + Assert.IsTrue(supports.Condition.Check(device)); + } + + [Test] + public void SupportsUppercaseOrKeywordRule() + { + var source = @"@supports ((background-transparency: half) OR (color: blue)) { }"; + var sheet = ParseStyleSheet(source); + var device = new DefaultRenderDevice(); + Assert.AreEqual(1, sheet.Rules.Length); + Assert.IsInstanceOf(sheet.Rules[0]); + var supports = sheet.Rules[0] as CssSupportsRule; + Assert.AreEqual("((background-transparency: half) or (color: blue))", supports.ConditionText); + Assert.IsTrue(supports.Condition.Check(device)); + } + + [Test] + public void SupportsMixedCaseAndKeywordChainRule() + { + var source = @"@supports ((background-color: red) And (color: blue) aND (width: 10px)) { }"; + var sheet = ParseStyleSheet(source); + var device = new DefaultRenderDevice(); + Assert.AreEqual(1, sheet.Rules.Length); + Assert.IsInstanceOf(sheet.Rules[0]); + var supports = sheet.Rules[0] as CssSupportsRule; + Assert.AreEqual("((background-color: red) and (color: blue) and (width: 10px))", supports.ConditionText); + Assert.IsTrue(supports.Condition.Check(device)); + } + + [Test] + public void SupportsUppercaseAndKeywordKeepsInnerRules() + { + var source = @"@supports (color: red) AND (display: flex) { + body { width: 100%; } +}"; + var sheet = ParseStyleSheet(source); + Assert.AreEqual(1, sheet.Rules.Length); + Assert.IsInstanceOf(sheet.Rules[0]); + var supports = sheet.Rules[0] as CssSupportsRule; + Assert.AreEqual(1, supports.Rules.Length); + } } } diff --git a/src/AngleSharp.Css/Parser/Micro/ConditionParser.cs b/src/AngleSharp.Css/Parser/Micro/ConditionParser.cs index 9fa66f65..55561c83 100644 --- a/src/AngleSharp.Css/Parser/Micro/ConditionParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/ConditionParser.cs @@ -57,8 +57,8 @@ private static IConditionFunction ConjunctionOrDisjunction(this StringSource sou if (ident != null) { - var isAnd = ident.Is(CssKeywords.And); - var isOr = ident.Is(CssKeywords.Or); + var isAnd = ident.Isi(CssKeywords.And); + var isOr = ident.Isi(CssKeywords.Or); if (isAnd || isOr) { @@ -141,7 +141,7 @@ private static IEnumerable Scan(this StringSource source, St source.SkipSpacesAndComments(); ident = source.ParseIdent(); } - while (ident != null && ident.Is(keyword)); + while (ident != null && ident.Isi(keyword)); return conditions; } diff --git a/src/AngleSharp.Css/Parser/Micro/KeyframeParser.cs b/src/AngleSharp.Css/Parser/Micro/KeyframeParser.cs index ca6bc7fa..8ea41c07 100644 --- a/src/AngleSharp.Css/Parser/Micro/KeyframeParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/KeyframeParser.cs @@ -43,11 +43,11 @@ public static IKeyframeSelector ParseKeyframeSelector(this StringSource source) stops.Add(test.Value); } - else if (id.Is(CssKeywords.From)) + else if (id.Isi(CssKeywords.From)) { stops.Add(0f); } - else if (id.Is(CssKeywords.To)) + else if (id.Isi(CssKeywords.To)) { stops.Add(1f); } From afbb9cdf756c250d8629f3d28ff05b84b18594bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 4 Sep 2026 21:00:38 -0400 Subject: [PATCH 2/2] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1b4e6a..9167d24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Released on Saturday, September 5 2026 - Fixed `not ` is always false (#231) - Fixed `calc()` computations in AoT-compiled applications (#236) @sebastienros - Fixed usage of `calc()` with unitless scaling (multiplication / division) +- Fixed case-sensitive matching of `and` / `or` in `@supports` and `from` / `to` in `@keyframes` (#240) - Added optional CSSOM compliant color seralization (#229) @lahma - Added user-preference media features to the render device (#235) @lahma - Added media query list evaluation using `IRenderDevice` (#228) @lahma