From c7d3610bfa13917d0abbf30d94f41719acbc5b06 Mon Sep 17 00:00:00 2001 From: Aditi Date: Tue, 28 Oct 2025 15:41:01 +0530 Subject: [PATCH] Add support for case sensitivity in attribute selector --- .../helger/css/decl/CSSSelectorAttribute.java | 28 ++++++++- .../helger/css/decl/ECSSAttributeCase.java | 62 +++++++++++++++++++ .../css/handler/CSSNodeToDomainObject.java | 24 +++++-- .../com/helger/css/handler/ECSSNodeType.java | 1 + ph-css/src/main/jjtree/ParserCSS30.jjt | 17 +++++ .../com/helger/css/decl/CSSStyleRuleTest.java | 6 +- 6 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 ph-css/src/main/java/com/helger/css/decl/ECSSAttributeCase.java diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorAttribute.java b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorAttribute.java index f82af397..cae959a2 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorAttribute.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorAttribute.java @@ -35,6 +35,7 @@ * A single CSS selector attribute. * * @see ECSSAttributeOperator + * @see ECSSAttributeCase * @author Philip Helger */ @NotThreadSafe @@ -44,6 +45,7 @@ public class CSSSelectorAttribute implements ICSSSelectorMember, ICSSSourceLocat private final String m_sAttrName; private final ECSSAttributeOperator m_eOperator; private final String m_sAttrValue; + private final ECSSAttributeCase m_eAttrCase; private CSSSourceLocation m_aSourceLocation; private static boolean _isValidNamespacePrefix (@Nullable final String sNamespacePrefix) @@ -61,12 +63,22 @@ public CSSSelectorAttribute (@Nullable final String sNamespacePrefix, @Nonnull @ m_sAttrName = sAttrName; m_eOperator = null; m_sAttrValue = null; + m_eAttrCase = null; } public CSSSelectorAttribute (@Nullable final String sNamespacePrefix, @Nonnull @Nonempty final String sAttrName, @Nonnull final ECSSAttributeOperator eOperator, @Nonnull final String sAttrValue) + { + this (sNamespacePrefix, sAttrName, eOperator, sAttrValue, null); + } + + public CSSSelectorAttribute (@Nullable final String sNamespacePrefix, + @Nonnull @Nonempty final String sAttrName, + @Nullable final ECSSAttributeOperator eOperator, + @Nullable final String sAttrValue, + @Nullable final ECSSAttributeCase eCaseFlag) { if (!_isValidNamespacePrefix (sNamespacePrefix)) throw new IllegalArgumentException ("namespacePrefix is illegal!"); @@ -78,6 +90,7 @@ public CSSSelectorAttribute (@Nullable final String sNamespacePrefix, m_sAttrName = sAttrName; m_eOperator = eOperator; m_sAttrValue = sAttrValue; + m_eAttrCase = eCaseFlag; } @Nullable @@ -105,6 +118,12 @@ public String getAttrValue () return m_sAttrValue; } + @Nullable + public ECSSAttributeCase getCaseSensitivityFlag () + { + return m_eAttrCase; + } + @Nonnull @Nonempty public String getAsCSSString (@Nonnull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) @@ -115,7 +134,11 @@ public String getAsCSSString (@Nonnull final ICSSWriterSettings aSettings, @Nonn aSB.append (m_sNamespacePrefix); aSB.append (m_sAttrName); if (m_eOperator != null) + { aSB.append (m_eOperator.getAsCSSString (aSettings, nIndentLevel)).append (m_sAttrValue); + if (m_eAttrCase != null) + aSB.append (' ').append (m_eAttrCase.getName ()); + } return aSB.append (']').toString (); } @@ -141,7 +164,8 @@ public boolean equals (final Object o) return EqualsHelper.equals (m_sNamespacePrefix, rhs.m_sNamespacePrefix) && m_sAttrName.equals (rhs.m_sAttrName) && EqualsHelper.equals (m_eOperator, rhs.m_eOperator) && - EqualsHelper.equals (m_sAttrValue, rhs.m_sAttrValue); + EqualsHelper.equals (m_sAttrValue, rhs.m_sAttrValue) && + EqualsHelper.equals (m_eAttrCase, rhs.m_eAttrCase); } @Override @@ -151,6 +175,7 @@ public int hashCode () .append (m_sAttrName) .append (m_eOperator) .append (m_sAttrValue) + .append (m_eAttrCase) .getHashCode (); } @@ -161,6 +186,7 @@ public String toString () .append ("attrName", m_sAttrName) .appendIfNotNull ("operator", m_eOperator) .appendIfNotNull ("attrValue", m_sAttrValue) + .appendIfNotNull ("caseFlag", m_eAttrCase) .appendIfNotNull ("SourceLocation", m_aSourceLocation) .getToString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/ECSSAttributeCase.java b/ph-css/src/main/java/com/helger/css/decl/ECSSAttributeCase.java new file mode 100644 index 00000000..a9620155 --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/ECSSAttributeCase.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014-2025 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.helger.css.decl; + +import com.helger.annotation.Nonempty; +import com.helger.annotation.Nonnegative; +import com.helger.base.lang.EnumHelper; +import com.helger.base.name.IHasName; +import com.helger.css.ICSSWriteable; +import com.helger.css.ICSSWriterSettings; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; + +/** + * Enumeration with case sensitivity flags as in + * [foo="bar" i] or [foo="bar" s] + * + * @author Aditi Singh + */ + +public enum ECSSAttributeCase implements ICSSWriteable, IHasName { + CASE_SENSITIVE("s"), + CASE_INSENSITIVE("i"); + + private final String m_sName; + + ECSSAttributeCase(@Nonnull @Nonempty final String sName) { + m_sName = sName; + } + + @Nonnull + @Nonempty + public String getName() { + return m_sName; + } + + @Nonnull + @Nonempty + public String getAsCSSString(@Nonnull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) { + return m_sName; + } + + @Nullable + public static ECSSAttributeCase getFromNameOrNull(@Nullable final String sName) { + return EnumHelper.getFromNameOrNull(ECSSAttributeCase.class, sName); + } +} diff --git a/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java b/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java index f09301fe..3d9a008e 100644 --- a/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java +++ b/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java @@ -166,13 +166,17 @@ private CSSSelectorAttribute _createSelectorAttribute (@Nonnull final CSSNode aN } else { - final int nExpectedChildCount = nOperatorIndex + 2; - if (nChildren != nExpectedChildCount) + // With operator and value + int nMin = nOperatorIndex + 2; + // With operator, value and case sensitivity flag + int nMax = nOperatorIndex + 3; + + if (nChildren < nMin || nChildren > nMax) _throwUnexpectedChildrenCount (aNode, "Illegal number of children present (" + nChildren + - ") - expected " + - nExpectedChildCount); + ") - expected it to be more than " + + nMin + " and less than " + nMax); // With operator... final CSSNode aOperator = aNode.jjtGetChild (nOperatorIndex); @@ -182,10 +186,20 @@ private CSSSelectorAttribute _createSelectorAttribute (@Nonnull final CSSNode aN final CSSNode aAttrValue = aNode.jjtGetChild (nOperatorIndex + 1); _expectNodeType (aAttrValue, ECSSNodeType.ATTRIBVALUE); + ECSSAttributeCase eCaseFlag = null; + // Optional case sensitivity flag + if (nChildren == nMax) + { + final CSSNode aFlag = aNode.jjtGetChild (nOperatorIndex + 2); + _expectNodeType (aFlag, ECSSNodeType.ATTRIBCASE); + eCaseFlag = ECSSAttributeCase.getFromNameOrNull (aFlag.getText ()); + } + ret = new CSSSelectorAttribute (sNamespacePrefix, sAttrName, ECSSAttributeOperator.getFromNameOrNull (aOperator.getText ()), - aAttrValue.getText ()); + aAttrValue.getText (), + eCaseFlag); } if (m_bUseSourceLocation) ret.setSourceLocation (aNode.getSourceLocation ()); diff --git a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java index 1a213391..bda7c1a5 100644 --- a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java +++ b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java @@ -68,6 +68,7 @@ public enum ECSSNodeType ATTRIB (ParserCSS30TreeConstants.JJTATTRIBUTESELECTOR), ATTRIBOPERATOR (ParserCSS30TreeConstants.JJTATTRIBOPERATOR), ATTRIBVALUE (ParserCSS30TreeConstants.JJTATTRIBVALUE), + ATTRIBCASE (ParserCSS30TreeConstants.JJTATTRIBCASE), // RELATIVE_SELECTOR (ParserCSS30TreeConstants.JJTRELATIVESELECTOR), SELECTORCOMBINATOR (ParserCSS30TreeConstants.JJTSELECTORCOMBINATOR), NTH (ParserCSS30TreeConstants.JJTNTH), diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index e9608f3c..6b2117fb 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -942,6 +942,21 @@ void attribValue() : ) } +void attribCase() : {} +{ + ( )* + ( + { + final String v = token.image; + if ("i".equalsIgnoreCase(v) || "s".equalsIgnoreCase(v)) + jjtThis.setText(v.toLowerCase(java.util.Locale.ROOT)); + else + throw new ParseException("Invalid case-sensitivity flag '" + v + + "'. Only 'i' or 's' are allowed."); + } + )? +} + void attributeSelector() : {} { @@ -957,7 +972,9 @@ void attributeSelector() : {} ( )* attribValue() ( )* + attribCase() )? + ( )* } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java index eb65baef..f7baae90 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java @@ -74,7 +74,7 @@ public void testRead1 () @Test public void testRead2 () { - CSSStyleRule aSR = _parse ("div, .colored, #my-red, #menu > .active, a[href^=red] { }"); + CSSStyleRule aSR = _parse ("div, .colored, #my-red, #menu > .active, a[href^=red i] { }"); assertEquals (5, aSR.getSelectorCount ()); assertEquals (1, aSR.getSelectorAtIndex (0).getMemberCount ()); @@ -94,6 +94,7 @@ public void testRead2 () assertEquals (2, aSR.getSelectorAtIndex (4).getMemberCount ()); assertTrue (aSR.getSelectorAtIndex (4).getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertTrue (aSR.getSelectorAtIndex (4).getMemberAtIndex (1) instanceof CSSSelectorAttribute); + assertEquals (ECSSAttributeCase.CASE_INSENSITIVE, ((CSSSelectorAttribute) aSR.getSelectorAtIndex (4).getMemberAtIndex (1)).getCaseSensitivityFlag ()); // Create the same rule by application final CSSStyleRule aCreated = new CSSStyleRule (); @@ -107,7 +108,8 @@ public void testRead2 () .addMember (new CSSSelectorAttribute (null, "href", ECSSAttributeOperator.BEGINMATCH, - "red"))); + "red", + ECSSAttributeCase.CASE_INSENSITIVE))); TestHelper.testDefaultImplementationWithEqualContentObject (aSR, aCreated); } }