From 3fc5eda2a8afb16c91e891e1f9be2a1dc18fedef Mon Sep 17 00:00:00 2001 From: Jeremie Bresson Date: Wed, 3 Dec 2025 09:18:53 +0100 Subject: [PATCH 01/21] [release] set version to "8.1.1-unblu-1" --- ph-css/pom.xml | 2 +- ph-csscompress-maven-plugin/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ph-css/pom.xml b/ph-css/pom.xml index 67182873..f96ba353 100644 --- a/ph-css/pom.xml +++ b/ph-css/pom.xml @@ -22,7 +22,7 @@ com.helger ph-css-parent-pom - 8.1.1-SNAPSHOT + 8.1.1-unblu-1 ph-css bundle diff --git a/ph-csscompress-maven-plugin/pom.xml b/ph-csscompress-maven-plugin/pom.xml index 902ca40a..d96261fc 100644 --- a/ph-csscompress-maven-plugin/pom.xml +++ b/ph-csscompress-maven-plugin/pom.xml @@ -22,7 +22,7 @@ com.helger ph-css-parent-pom - 8.1.1-SNAPSHOT + 8.1.1-unblu-1 com.helger.maven ph-csscompress-maven-plugin diff --git a/pom.xml b/pom.xml index 63239e1d..a8aef3f7 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 3.0.2 ph-css-parent-pom - 8.1.1-SNAPSHOT + 8.1.1-unblu-1 pom ph-css-parent-pom Base POM to build the ph-css projects From 4fe3702cf2f3d710e1f00c7821e34e3c6e3b5d81 Mon Sep 17 00:00:00 2001 From: Jeremie Bresson Date: Wed, 3 Dec 2025 09:19:46 +0100 Subject: [PATCH 02/21] Set version to "8.1.1-SNAPSHOT" This reverts commit 3fc5eda2a8afb16c91e891e1f9be2a1dc18fedef. --- ph-css/pom.xml | 2 +- ph-csscompress-maven-plugin/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ph-css/pom.xml b/ph-css/pom.xml index f96ba353..67182873 100644 --- a/ph-css/pom.xml +++ b/ph-css/pom.xml @@ -22,7 +22,7 @@ com.helger ph-css-parent-pom - 8.1.1-unblu-1 + 8.1.1-SNAPSHOT ph-css bundle diff --git a/ph-csscompress-maven-plugin/pom.xml b/ph-csscompress-maven-plugin/pom.xml index d96261fc..902ca40a 100644 --- a/ph-csscompress-maven-plugin/pom.xml +++ b/ph-csscompress-maven-plugin/pom.xml @@ -22,7 +22,7 @@ com.helger ph-css-parent-pom - 8.1.1-unblu-1 + 8.1.1-SNAPSHOT com.helger.maven ph-csscompress-maven-plugin diff --git a/pom.xml b/pom.xml index a8aef3f7..63239e1d 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 3.0.2 ph-css-parent-pom - 8.1.1-unblu-1 + 8.1.1-SNAPSHOT pom ph-css-parent-pom Base POM to build the ph-css projects From 0f4b10d4d6ffdd52cb13138d3bf0c9f0824a51b9 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Thu, 2 Apr 2026 15:55:25 +0200 Subject: [PATCH 03/21] #121 add support for CSS Houdini @property rule --- .../com/helger/css/ICSSWriterSettings.java | 5 + .../com/helger/css/decl/CSSPropertyRule.java | 223 ++++++++++++++++++ .../css/decl/CSSPropertyRuleDeclaration.java | 111 +++++++++ .../com/helger/css/decl/visit/CSSVisitor.java | 36 ++- .../css/decl/visit/CSSVisitorForUrl.java | 15 ++ .../css/decl/visit/DefaultCSSVisitor.java | 14 ++ .../helger/css/decl/visit/ICSSVisitor.java | 22 ++ .../css/handler/CSSNodeToDomainObject.java | 91 +++++-- .../com/helger/css/handler/ECSSNodeType.java | 4 + .../helger/css/writer/CSSWriterSettings.java | 19 ++ ph-css/src/main/jjtree/ParserCSS30.jjt | 64 +++++ 11 files changed, 581 insertions(+), 23 deletions(-) create mode 100644 ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java create mode 100644 ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java diff --git a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java index 88e46c41..878069cc 100644 --- a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java +++ b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java @@ -107,6 +107,11 @@ public interface ICSSWriterSettings */ boolean isWriteSupportsRules (); + /** + * @return true if @property rules should be written, false if not + */ + boolean isWritePropertyRules (); + /** * @return true if unknown @ rules should be written, false if not */ diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java new file mode 100644 index 00000000..743c0a6c --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2014-2026 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 org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import com.helger.annotation.Nonempty; +import com.helger.annotation.Nonnegative; +import com.helger.annotation.style.ReturnsMutableCopy; +import com.helger.base.enforce.ValueEnforcer; +import com.helger.base.hashcode.HashCodeGenerator; +import com.helger.base.state.EChange; +import com.helger.base.string.StringHelper; +import com.helger.base.tostring.ToStringGenerator; +import com.helger.collection.commons.CommonsArrayList; +import com.helger.collection.commons.ICommonsList; +import com.helger.css.CSSSourceLocation; +import com.helger.css.ICSSSourceLocationAware; +import com.helger.css.ICSSWriterSettings; + +/** + * Represents a single @property rule: a list of style rules only valid when a certain + * declaration is available. See {@link com.helger.css.ECSSSpecification#CSS3_CONDITIONAL}
+ * Example:
+ * @supports (transition-property: color) { + div { color:red; } +} + * + * @author Mike Wiedenbauer + */ +public class CSSPropertyRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware +{ + private final String m_sIdentifier; + private final ICommonsList m_aDeclarations = new CommonsArrayList <> (); + private CSSSourceLocation m_aSourceLocation; + + public static boolean isValidIdentifier (@NonNull @Nonempty final String sIdentifier) + { + return StringHelper.startsWith (sIdentifier, "--"); + } + + public CSSPropertyRule (@NonNull @Nonempty final String sIdentifier) + { + ValueEnforcer.isTrue (isValidIdentifier (sIdentifier), "Identifier is invalid"); + m_sIdentifier = sIdentifier; + } + + @NonNull + @Nonempty + public String getIdentifier () + { + return m_sIdentifier; + } + + @NonNull + public CSSPropertyRule addPropertyRuleDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + ValueEnforcer.notNull (aDeclaration, "PropertyRuleDeclaration"); + + m_aDeclarations.add (aDeclaration); + return this; + } + + @NonNull + public CSSPropertyRule addPropertyRuleDeclaration (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + ValueEnforcer.isGE0(nIndex, "Index"); + ValueEnforcer.notNull (aDeclaration, "PropertyRuleDeclaration"); + + m_aDeclarations.add (nIndex, aDeclaration); + return this; + } + + @NonNull + public EChange removePropertyRuleDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + return m_aDeclarations.removeObject (aDeclaration); + } + + @NonNull + public EChange removePropertyRuleDeclaration (@Nonnegative final int nIndex) + { + return m_aDeclarations.removeAtIndex (nIndex); + } + + @NonNull + public EChange removeAllPropertyRuleDeclarations () + { + return m_aDeclarations.removeAll (); + } + + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllPropertyRuleDeclarations () + { + return m_aDeclarations.getClone(); + } + + @Nullable + public CSSPropertyRuleDeclaration getPropertyRuleDeclarationAtIndex (@Nonnegative final int nIndex) + { + return m_aDeclarations.getAtIndex (nIndex); + } + + @NonNull + public CSSPropertyRule setMemberAtIndex (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aNewDeclaration) + { + m_aDeclarations.set (nIndex, aNewDeclaration); + return this; + } + + public boolean hashPropertyRuleDeclarations () + { + return m_aDeclarations.isNotEmpty (); + } + + @Nonnegative + public int getPropertyRuleDeclarationCount () + { + return m_aDeclarations.size (); + } + + @NonNull + @Nonempty + public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) + { + // Always ignore Property rules? + if (!aSettings.isWritePropertyRules ()) + return ""; + + final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); + final int nMemberCount = m_aDeclarations.size(); + + if (aSettings.isRemoveUnnecessaryCode () && nMemberCount == 0) + return ""; + + final StringBuilder aSB = new StringBuilder ("@property ").append (m_sIdentifier); + if (nMemberCount == 0) + { + aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); + } + else + { + // At least one descriptor present + aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); + boolean bFirst = true; + for (final CSSPropertyRuleDeclaration aDeclaration : m_aDeclarations) + { + final String sDescriptorCSS = aDeclaration.getAsCSSString (aSettings, nIndentLevel + 1); + if (StringHelper.isNotEmpty (sDescriptorCSS)) + { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aSB.append (aSettings.getNewLineString ()); + + if (!bOptimizedOutput) + aSB.append (aSettings.getIndent (nIndentLevel + 1)); + aSB.append (sDescriptorCSS); + } + } + if (!bOptimizedOutput) + aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append ('}'); + if (!bOptimizedOutput) + aSB.append (aSettings.getNewLineString ()); + } + return aSB.toString(); + } + + @Nullable + public final CSSSourceLocation getSourceLocation () + { + return m_aSourceLocation; + } + + public final void setSourceLocation (@Nullable final CSSSourceLocation aSourceLocation) + { + m_aSourceLocation = aSourceLocation; + } + + @Override + public boolean equals (final Object o) + { + if (o == this) + return true; + if (o == null || !getClass ().equals (o.getClass ())) + return false; + final CSSPropertyRule rhs = (CSSPropertyRule) o; + return m_sIdentifier.equals (rhs.m_sIdentifier) && m_aDeclarations.equals (rhs.m_aDeclarations); + } + + @Override + public int hashCode () + { + return new HashCodeGenerator (this).append (m_sIdentifier).append (m_aDeclarations).getHashCode (); + } + + @Override + public String toString () + { + return new ToStringGenerator (this).append ("identifier", m_sIdentifier) + .append ("declaration", m_aDeclarations) + .appendIfNotNull ("SourceLocation", m_aSourceLocation) + .getToString (); + } +} diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java new file mode 100644 index 00000000..03f361a9 --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java @@ -0,0 +1,111 @@ +package com.helger.css.decl; + +import java.util.Locale; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import com.helger.annotation.Nonempty; +import com.helger.annotation.Nonnegative; +import com.helger.annotation.style.ReturnsMutableObject; +import com.helger.base.enforce.ValueEnforcer; +import com.helger.base.hashcode.HashCodeGenerator; +import com.helger.base.tostring.ToStringGenerator; +import com.helger.css.CCSS; +import com.helger.css.CSSSourceLocation; +import com.helger.css.ICSSSourceLocationAware; +import com.helger.css.ICSSWriterSettings; + +public class CSSPropertyRuleDeclaration implements ICSSSourceLocationAware +{ + private String m_sDescriptor; + private CSSExpression m_aExpression; + private CSSSourceLocation m_aSourceLocation; + + public CSSPropertyRuleDeclaration (@NonNull @Nonempty final String sDescriptor, @NonNull final CSSExpression aExpression) + { + setDescriptor(sDescriptor); + setExpression(aExpression); + } + + @NonNull + @Nonempty + public final String getDescriptor () + { + return m_sDescriptor; + } + + @NonNull + public final CSSPropertyRuleDeclaration setDescriptor (@NonNull @Nonempty final String sDescriptor) + { + ValueEnforcer.notEmpty (sDescriptor, "Descriptor"); + m_sDescriptor = sDescriptor.toLowerCase (Locale.ROOT); + return this; + } + + @NonNull + @ReturnsMutableObject + public final CSSExpression getExpression () + { + return m_aExpression; + } + + @NonNull + public final String getExpressionAsCSSString () + { + return m_aExpression.getAsCSSString (); + } + + @NonNull + public final CSSPropertyRuleDeclaration setExpression (@NonNull final CSSExpression aExpression) + { + m_aExpression = ValueEnforcer.notNull (aExpression, "Expression"); + return this; + } + + @NonNull + @Nonempty + public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) + { + return m_sDescriptor + + CCSS.SEPARATOR_PROPERTY_VALUE + + m_aExpression.getAsCSSString (aSettings, nIndentLevel); + } + + @Nullable + public final CSSSourceLocation getSourceLocation () + { + return m_aSourceLocation; + } + + public final void setSourceLocation (@Nullable final CSSSourceLocation aSourceLocation) + { + m_aSourceLocation = aSourceLocation; + } + + @Override + public boolean equals (final Object o) + { + if (o == this) + return true; + if (o == null || !getClass ().equals (o.getClass ())) + return false; + final CSSPropertyRuleDeclaration rhs = (CSSPropertyRuleDeclaration) o; + return m_sDescriptor.equals (rhs.m_sDescriptor) && m_aExpression.equals (rhs.m_aExpression); + } + + @Override + public int hashCode () + { + return new HashCodeGenerator (this).append (m_sDescriptor).append (m_aExpression).getHashCode (); + } + + @Override + public String toString () + { + return new ToStringGenerator (this).append ("descriptor", m_sDescriptor) + .append ("expression", m_aExpression) + .appendIfNotNull ("SourceLocation", m_aSourceLocation) + .getToString (); + } +} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java index 42ad533c..9a27d975 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java @@ -34,6 +34,8 @@ import com.helger.css.decl.CSSSelector; import com.helger.css.decl.CSSStyleRule; import com.helger.css.decl.CSSSupportsRule; +import com.helger.css.decl.CSSPropertyRule; +import com.helger.css.decl.CSSPropertyRuleDeclaration; import com.helger.css.decl.CSSUnknownRule; import com.helger.css.decl.CSSViewportRule; import com.helger.css.decl.CascadingStyleSheet; @@ -311,6 +313,29 @@ public static void visitLayerRule (@NonNull final CSSLayerRule aLayerRule, @NonN } } + /** + * Visit all elements of a single property rule. + * + * @param aPropertyRule + * The property rule to visit. May not be null. + * @param aVisitor + * The visitor to use. May not be null. + */ + public static void visitPropertyRule (@NonNull final CSSPropertyRule aPropertyRule, @NonNull final ICSSVisitor aVisitor) + { + aVisitor.onBeginPropertyRule (aPropertyRule); + try + { + // for all property rule declarations + for (final CSSPropertyRuleDeclaration aDeclaration : aPropertyRule.getAllPropertyRuleDeclarations ()) + aVisitor.onPropertyRuleDeclaration (aDeclaration); + } + finally + { + aVisitor.onEndPropertyRule (aPropertyRule); + } + } + /** * Visit all elements of a single unknown @ rule. * @@ -375,12 +400,17 @@ public static void visitTopLevelRule (@NonNull final ICSSTopLevelRule aTopLevelR visitLayerRule ((CSSLayerRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSUnknownRule) + if (aTopLevelRule instanceof CSSPropertyRule) { - visitUnknownRule ((CSSUnknownRule) aTopLevelRule, aVisitor); + visitPropertyRule((CSSPropertyRule) aTopLevelRule, aVisitor); } else - throw new IllegalStateException ("Top level rule " + aTopLevelRule + " is unsupported!"); + if (aTopLevelRule instanceof CSSUnknownRule) + { + visitUnknownRule ((CSSUnknownRule) aTopLevelRule, aVisitor); + } + else + throw new IllegalStateException ("Top level rule " + aTopLevelRule + " is unsupported!"); } /** diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitorForUrl.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitorForUrl.java index 3d055e50..b32e7984 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitorForUrl.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitorForUrl.java @@ -140,6 +140,11 @@ public void onDeclaration (@NonNull final CSSDeclaration aDeclaration) _recursiveCheckExpression (aTopLevelRule, aDeclaration, aDeclaration.getExpression ()); } + public void onPropertyRuleDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + // No action + } + public void onBeginStyleRule (@NonNull final CSSStyleRule aStyleRule) { m_aTopLevelRule.push (aStyleRule); @@ -245,6 +250,16 @@ public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule) m_aTopLevelRule.pop(); } + public void onBeginPropertyRule (@NonNull final CSSPropertyRule aPropertyRule) + { + m_aTopLevelRule.push(aPropertyRule); + } + + public void onEndPropertyRule (@NonNull final CSSPropertyRule aPropertyRule) + { + m_aTopLevelRule.pop(); + } + public void onUnknownRule (@NonNull final CSSUnknownRule aUnknownRule) { // no action diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/DefaultCSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/DefaultCSSVisitor.java index d857624b..8897f1c0 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/DefaultCSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/DefaultCSSVisitor.java @@ -33,6 +33,8 @@ import com.helger.css.decl.CSSSelector; import com.helger.css.decl.CSSStyleRule; import com.helger.css.decl.CSSSupportsRule; +import com.helger.css.decl.CSSPropertyRule; +import com.helger.css.decl.CSSPropertyRuleDeclaration; import com.helger.css.decl.CSSUnknownRule; import com.helger.css.decl.CSSViewportRule; @@ -64,6 +66,10 @@ public void onNamespace (@NonNull final CSSNamespaceRule aNamespaceRule) public void onDeclaration (@NonNull final CSSDeclaration aDeclaration) {} + @OverrideOnDemand + public void onPropertyRuleDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + {} + @OverrideOnDemand public void onBeginStyleRule (@NonNull final CSSStyleRule aStyleRule) {} @@ -148,6 +154,14 @@ public void onBeginLayerRule (@NonNull final CSSLayerRule aLayerRule) public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule) {} + @OverrideOnDemand + public void onBeginPropertyRule (@NonNull final CSSPropertyRule aPropertyRule) + {} + + @OverrideOnDemand + public void onEndPropertyRule (@NonNull final CSSPropertyRule aLayerRule) + {} + @OverrideOnDemand public void onUnknownRule (@NonNull final CSSUnknownRule aUnknownRule) {} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java index 002f7d14..52ac9929 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java @@ -31,6 +31,8 @@ import com.helger.css.decl.CSSSelector; import com.helger.css.decl.CSSStyleRule; import com.helger.css.decl.CSSSupportsRule; +import com.helger.css.decl.CSSPropertyRule; +import com.helger.css.decl.CSSPropertyRuleDeclaration; import com.helger.css.decl.CSSUnknownRule; import com.helger.css.decl.CSSViewportRule; @@ -73,6 +75,9 @@ public interface ICSSVisitor */ void onDeclaration (@NonNull CSSDeclaration aDeclaration); + + void onPropertyRuleDeclaration(@NonNull CSSPropertyRuleDeclaration aDeclaration); + // style rules: /** * Called when a style rule starts.
@@ -262,6 +267,23 @@ public interface ICSSVisitor */ void onEndLayerRule (@NonNull CSSLayerRule aLayerRule); + // property rules + /** + * Called when a property rule starts. + * + * @param aPropertyRule + * The property rule. Never null. + */ + void onBeginPropertyRule (@NonNull CSSPropertyRule aPropertyRule); + + /** + * Called when a property rule ends. + * + * @param aPropertyRule + * The property rule. Never null. + */ + void onEndPropertyRule (@NonNull CSSPropertyRule aPropertyRule); + // unknown rules /** * Called when an unknown rule is encountered. 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 d34adccc..24990009 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 @@ -1382,6 +1382,54 @@ private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) return ret; } + @NonNull + private CSSPropertyRuleDeclaration _createPropertyRuleDeclaration (@NonNull final CSSNode aNode) + { + _expectNodeType(aNode, ECSSNodeType.PROPERTYRULEDECLARATION); + final int nChildCount = aNode.jjtGetNumChildren (); + if (nChildCount != 2) + _throwUnexpectedChildrenCount (aNode, "Expected 2 children but got " + nChildCount + "!"); + + if (nChildCount == 1) + { + // Syntax error. E.g. "syntax:;" + return null; + } + + final String sDescriptor = aNode.jjtGetChild (0).getText (); + if (sDescriptor == null) + { + // Syntax error with deprecated property name (see #84) + return null; + } + + final CSSExpression aExpression = _createExpression (aNode.jjtGetChild (1)); + final CSSPropertyRuleDeclaration ret = new CSSPropertyRuleDeclaration (sDescriptor, aExpression); + if (m_bUseSourceLocation) + ret.setSourceLocation (aNode.getSourceLocation ()); + return ret; + } + + @NonNull + private CSSPropertyRule _createPropertyRule (@NonNull final CSSNode aNode) + { + _expectNodeType (aNode, ECSSNodeType.PROPERTYRULE); + final int nChildCount = aNode.jjtGetNumChildren (); + if (nChildCount > 3) + _throwUnexpectedChildrenCount (aNode, + "Expected at most 3 children but got " + nChildCount + "!"); + + // Get the identifier (e.g. "--canBeAnything") + final String sIdentifier = aNode.getText (); + + final CSSPropertyRule ret = new CSSPropertyRule (sIdentifier); + if (m_bUseSourceLocation) + ret.setSourceLocation (aNode.getSourceLocation ()); + for (final CSSNode aChildNode : aNode) + ret.addPropertyRuleDeclaration (_createPropertyRuleDeclaration (aChildNode)); + return ret; + } + @NonNull private CSSUnknownRule _createUnknownRule (@NonNull final CSSNode aNode) { @@ -1455,30 +1503,33 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) ret.addRule (_createSupportsRule (aChildNode)); else - if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) - { - // Unknown rule indicates either - // 1. a parsing error - // 2. a non-standard rule - ret.addRule (_createUnknownRule (aChildNode)); - } + if (ECSSNodeType.PROPERTYRULE.isNode (aChildNode)) + ret.addRule (_createPropertyRule (aChildNode)); else - if (ECSSNodeType.ROOT.isNode (aChildNode)) + if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) { - /* - * In case a parsing error occurs (as e.g. happening in issue #41) - * and browser compliant mode is enabled, some CSS code is skipped - * and a retry happens. This retry will be a recursive stylesheet - * object that is a child of the previous stylesheet but "flattened" - * for the result object. - */ - _recursiveFillCascadingStyleSheetFromNode (aChildNode, ret); + // Unknown rule indicates either + // 1. a parsing error + // 2. a non-standard rule + ret.addRule (_createUnknownRule (aChildNode)); } else - m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + - ECSSNodeType.getNodeName (aNode) + - ": " + - ECSSNodeType.getNodeName (aChildNode)); + if (ECSSNodeType.ROOT.isNode (aChildNode)) + { + /* + * In case a parsing error occurs (as e.g. happening in issue #41) + * and browser compliant mode is enabled, some CSS code is skipped + * and a retry happens. This retry will be a recursive stylesheet + * object that is a child of the previous stylesheet but "flattened" + * for the result object. + */ + _recursiveFillCascadingStyleSheetFromNode (aChildNode, ret); + } + else + m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + + ECSSNodeType.getNodeName (aNode) + + ": " + + ECSSNodeType.getNodeName (aChildNode)); } } 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 40b7591d..ef83783e 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 @@ -120,6 +120,10 @@ public enum ECSSNodeType SUPPORTSCONDITIONOPERATOR (ParserCSS30TreeConstants.JJTSUPPORTSCONDITIONOPERATOR), SUPPORTSNEGATION (ParserCSS30TreeConstants.JJTSUPPORTSNEGATION), SUPPORTSCONDITIONINPARENS (ParserCSS30TreeConstants.JJTSUPPORTSCONDITIONINPARENS), + // property + PROPERTYRULE (ParserCSS30TreeConstants.JJTPROPERTYRULE), + PROPERTYRULEDECLARATION (ParserCSS30TreeConstants.JJTPROPERTYRULEDECLARATION), + PROPERTYRULEDESCRIPTOR (ParserCSS30TreeConstants.JJTPROPERTYRULEDESCRIPTOR), // rest ERROR_SKIPTO (ParserCSS30TreeConstants.JJTERRORSKIPTO); diff --git a/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java index 9997b1b8..1da89358 100644 --- a/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java +++ b/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java @@ -65,6 +65,8 @@ public class CSSWriterSettings implements ICSSWriterSettings, ICloneable "-viewport" | "@viewport" > | < SUPPORTS_SYM: "@supports" > +| < PROPERTY_SYM: "@property" > | < AT_UNKNOWN: "@" | "@" > : IN_UNKNOWN_RULE | < IMPORTANT_SYM: "!" ( )* "important" > @@ -252,6 +253,11 @@ TOKEN : | < FROM_SYM: "from" > | < TO_SYM: "to" > | < OR_SYM: "or" > +| < TRUE_SYM: "true" > +| < FALSE_SYM: "false" > +| < SYNTAX_SYM: "syntax" > +| < INHERITS_SYM: "inherits" > +| < INITIALVALUE_SYM: "initial-value" > // ident (--|-|$|*)?{nmstart}{nmchar}* // Note: "*" hack for IE <= 6 was altered in 6.2.1 @@ -573,6 +579,7 @@ try{ | keyframesRule() | viewportRule() | supportsRule() + | propertyRule() | unknownRule() | charsetRule() { errorUnexpectedRule ("@charset", "charset rule in the middle of the file is not allowed!"); } | importRule() { errorUnexpectedRule ("@import", "import rule in the middle of the file is not allowed!"); } @@ -651,6 +658,14 @@ String anyIdentifier() #void : {} { return token.image; } } +String booleanOperator() #void : {} +{ + ( + | + ) + { return token.image; } +} + void url() : {} { { jjtThis.setText (token.image); } @@ -791,6 +806,7 @@ void exprTerm() : // Hack to allow "from" and "to" as identifiers (e.g. in linear-gradient) // Also allow "or" as parameter to "x:lang(no)" | sPrefix = anyIdentifier() + | sPrefix = booleanOperator() | url() | t = | function() @@ -1220,6 +1236,7 @@ void styleDeclarationOrRule() #void : {} | keyframesRule() { errorUnexpectedRule ("@keyframes", "keyframes rule in the middle of a rule-set is not allowed!"); } | viewportRule() { errorUnexpectedRule ("@viewport", "viewport rule in the middle of a rule-set is not allowed!"); } | supportsRule() { errorUnexpectedRule ("@supports", "supports rule in the middle of a rule-set is not allowed!"); } + | propertyRule() { errorUnexpectedRule ("@property", "property rule in the middle of a rule-set is not allowed!"); } | unknownRule() { errorUnexpectedRule ("@", "Unknown rule in the middle of a rule-set is not allowed!"); } | charsetRule() { errorUnexpectedRule ("@charset", "charset rule in the middle of a rule-set is not allowed!"); } | importRule() { errorUnexpectedRule ("@import", "import rule in the middle of a rule-set is not allowed!"); } @@ -1360,6 +1377,7 @@ void mediaRuleList() #void : {} | charsetRule() { errorUnexpectedRule ("@charset", "charset rule in the middle of a @media rule is not allowed!"); } | importRule() { errorUnexpectedRule ("@import", "import rule in the middle of a @media rule is not allowed!"); } | namespaceRule() { errorUnexpectedRule ("@namespace", "namespace rule in the middle of a @media rule is not allowed!"); } + | propertyRule() { errorUnexpectedRule ("@property", "property rule in the middle of a @media rule is not allowed!"); } ) ( )* )+ @@ -1507,6 +1525,7 @@ void layerBody() #void : {} | charsetRule() { errorUnexpectedRule ("@charset", "charset rule in the middle of a @layer rule is not allowed!"); } | importRule() { errorUnexpectedRule ("@import", "import rule in the middle of a @layer rule is not allowed!"); } | namespaceRule() { errorUnexpectedRule ("@namespace", "namespace rule in the middle of a @layer rule is not allowed!"); } + | propertyRule() { errorUnexpectedRule ("@property", "property rule in the middle of a @layer rule is not allowed!"); } ) ( )* )+ @@ -1674,6 +1693,7 @@ void supportsRuleBodyRule() #void : {} | charsetRule() { errorUnexpectedRule ("@charset", "charset rule in the middle of a @supports rule is not allowed!"); } | importRule() { errorUnexpectedRule ("@import", "import rule in the middle of a @supports rule is not allowed!"); } | namespaceRule() { errorUnexpectedRule ("@namespace", "namespace rule in the middle of a @supports rule is not allowed!"); } + | propertyRule() { errorUnexpectedRule ("@property", "property rule in the middle of a @supports rule is not allowed!"); } | viewportRule() { errorUnexpectedRule ("@viewport", "viewport rule in the middle of a @supports rule is not allowed!"); } ) } @@ -1704,6 +1724,50 @@ void supportsRule() : {} supportsRuleBody() } +void propertyRuleDescriptor() : {} +{ + ( { jjtThis.setText (token.image); } + | { jjtThis.setText (token.image); } + | { jjtThis.setText (token.image); } + ) +} + +void propertyRuleDeclaration() : {} +{ + propertyRuleDescriptor() + ( )* + + ( )* + expr() + +} + +void propertyRule() : +{ Token t; } +{ + + ( )* + t = + { + if (!t.image.startsWith ("--")) + throw new ParseException ("Exprected dashed identifier"); + jjtThis.setText (t.image); + } + ( )* + +try { + ( )* + ( propertyRuleDeclaration() + ( )* + )* + +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipInRule (ex); + else + errorSkipTo (ex, RBRACE); +} +} // // Unknown rule // From c420cc95eee2c8bc419f464508892dcc3b8572cc Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Thu, 2 Apr 2026 21:38:56 +0200 Subject: [PATCH 04/21] #121 simplified variation of EXPR for property rule declaration --- ph-css/src/main/jjtree/ParserCSS30.jjt | 28 +++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 981042bf..ee3bbfaa 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -806,7 +806,6 @@ void exprTerm() : // Hack to allow "from" and "to" as identifiers (e.g. in linear-gradient) // Also allow "or" as parameter to "x:lang(no)" | sPrefix = anyIdentifier() - | sPrefix = booleanOperator() | url() | t = | function() @@ -1724,6 +1723,30 @@ void supportsRule() : {} supportsRuleBody() } +void propertyRuleSimpleExpressionTerm() #exprterm : +{ String sStr; } +{ + ( sStr = string() + | sStr = booleanOperator() + ) + { + if (sStr != null) + jjtThis.setText (sStr); + } +} + +void propertyRuleSimpleExpression() #expr : {} +{ + propertyRuleSimpleExpressionTerm() +} + +void propertyRuleExpression() #void : {} +{ + ( propertyRuleSimpleExpression() + | expr() + ) +} + void propertyRuleDescriptor() : {} { ( { jjtThis.setText (token.image); } @@ -1731,14 +1754,13 @@ void propertyRuleDescriptor() : {} | { jjtThis.setText (token.image); } ) } - void propertyRuleDeclaration() : {} { propertyRuleDescriptor() ( )* ( )* - expr() + propertyRuleExpression() } From 37b410495d8edf7ed6c0a5fcc49784842364d21e Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Thu, 2 Apr 2026 21:47:04 +0200 Subject: [PATCH 05/21] #121 correct exception message --- ph-css/src/main/jjtree/ParserCSS30.jjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index ee3bbfaa..0bc6b1fb 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1772,7 +1772,7 @@ void propertyRule() : t = { if (!t.image.startsWith ("--")) - throw new ParseException ("Exprected dashed identifier"); + throw new ParseException ("Invalid identifier '" + t.image + "'. Only dashed identifier allowed (e.g. '--canBeAnything')."); jjtThis.setText (t.image); } ( )* From 1097431a682e50ec82370bf7c80bb4cf4cb19562 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Fri, 3 Apr 2026 11:05:44 +0200 Subject: [PATCH 06/21] #121 catch exception for invalid declaration (invalid declarations should be just ignored) --- ph-css/src/main/jjtree/ParserCSS30.jjt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 0bc6b1fb..b37e1126 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1754,14 +1754,21 @@ void propertyRuleDescriptor() : {} | { jjtThis.setText (token.image); } ) } + void propertyRuleDeclaration() : {} { +try { propertyRuleDescriptor() - ( )* ( )* propertyRuleExpression() +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else + errorSkipTo (ex, SEMICOLON); +} } void propertyRule() : From 12de6379008cca649eedf1c61b0e9a5ce67d2f07 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Fri, 3 Apr 2026 23:40:59 +0200 Subject: [PATCH 07/21] #121 fix handling of invalid descriptors --- ph-css/src/main/jjtree/ParserCSS30.jjt | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index b37e1126..96361ba9 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1747,12 +1747,20 @@ void propertyRuleExpression() #void : {} ) } +void propertyRuleUnknownDescriptor() #void : {} +{ + { throw new ParseException ("Invalid identifier '" + token.image + "'"); } +} + void propertyRuleDescriptor() : {} { - ( { jjtThis.setText (token.image); } - | { jjtThis.setText (token.image); } - | { jjtThis.setText (token.image); } + ( ( { jjtThis.setText (token.image); } + | { jjtThis.setText (token.image); } + | { jjtThis.setText (token.image); } + ) + ( )* ) + | propertyRuleUnknownDescriptor() } void propertyRuleDeclaration() : {} @@ -1762,12 +1770,15 @@ try { ( )* propertyRuleExpression() - } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); - else + else { errorSkipTo (ex, SEMICOLON); + token_source.backup(1); + } + jjtree.closeNodeScope (jjtn000, false); + jjtc000 = false; } } @@ -1787,6 +1798,7 @@ void propertyRule() : try { ( )* ( propertyRuleDeclaration() + ( )* )* From 37f131581a4d93028cea41db5e062dd2371f56f0 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Sat, 4 Apr 2026 01:12:50 +0200 Subject: [PATCH 08/21] #121 drop invalid descriptors properly --- ph-css/src/main/jjtree/ParserCSS30.jjt | 36 ++++++++++++-------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 96361ba9..29161a71 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1747,39 +1747,33 @@ void propertyRuleExpression() #void : {} ) } -void propertyRuleUnknownDescriptor() #void : {} +void propertyRuleInvalidDeclaration() #void : {} { - { throw new ParseException ("Invalid identifier '" + token.image + "'"); } +try { + { throw new ParseException ("Invalid descriptor '" + token.image + "'"); } +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else + errorSkipTo (ex, SEMICOLON); +} } void propertyRuleDescriptor() : {} { - ( ( { jjtThis.setText (token.image); } - | { jjtThis.setText (token.image); } - | { jjtThis.setText (token.image); } - ) - ( )* + ( { jjtThis.setText (token.image); } + | { jjtThis.setText (token.image); } + | { jjtThis.setText (token.image); } ) - | propertyRuleUnknownDescriptor() + ( )* } void propertyRuleDeclaration() : {} { -try { propertyRuleDescriptor() ( )* propertyRuleExpression() -} catch (/*final*/ ParseException ex) { - if (m_bBrowserCompliantMode) - browserCompliantSkipDecl (ex); - else { - errorSkipTo (ex, SEMICOLON); - token_source.backup(1); - } - jjtree.closeNodeScope (jjtn000, false); - jjtc000 = false; -} } void propertyRule() : @@ -1797,7 +1791,9 @@ void propertyRule() : try { ( )* - ( propertyRuleDeclaration() + ( ( propertyRuleDeclaration() + | propertyRuleInvalidDeclaration() + ) ( )* )* From 70696bb84e9bdb536cf616e7af43747b831efc2f Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Sun, 5 Apr 2026 23:08:11 +0200 Subject: [PATCH 09/21] #121 add IN_PROPERTY_RULE lexer state --- ph-css/src/main/jjtree/ParserCSS30.jjt | 45 +++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 29161a71..a7a994c4 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -89,7 +89,27 @@ SPECIAL_TOKEN : < "*/" > : DEFAULT } - + +TOKEN : +{ + < PLUS: "+" > +| < MINUS: "-" > +| < LROUND: "(" > +| < RROUND: ")" > + { + if (curLexState == IN_NTH) + SwitchTo(DEFAULT); + } +} + + +TOKEN : +{ + < LBRACE: "{" > +| < RBRACE: "}" > : DEFAULT +} + + TOKEN : { < S: (" "|"\t"|"\n"|"\r\n"|"\r"|"\f")+ > @@ -106,16 +126,15 @@ TOKEN : | < INTEGER: ( ["0"-"9"] )+ > } - + TOKEN : { - < PLUS: "+" > -| < MINUS: "-" > -| < LROUND: "(" > -| < RROUND: ")" > : DEFAULT + < SYNTAX_SYM: "syntax" > +| < INHERITS_SYM: "inherits" > +| < INITIALVALUE_SYM: "initial-value" > } - + TOKEN : { // private reg ex come first (no actions possible!) @@ -187,8 +206,6 @@ TOKEN : > | < NUMBER: > -| < LBRACE: "{" > -| < RBRACE: "}" > | < LSQUARE: "[" > | < RSQUARE: "]" > | < DOT: "." > @@ -240,7 +257,7 @@ TOKEN : | < VIEWPORT_SYM: "@-" "-viewport" | "@viewport" > | < SUPPORTS_SYM: "@supports" > -| < PROPERTY_SYM: "@property" > +| < PROPERTY_SYM: "@property" > : IN_PROPERTY_RULE | < AT_UNKNOWN: "@" | "@" > : IN_UNKNOWN_RULE | < IMPORTANT_SYM: "!" ( )* "important" > @@ -255,9 +272,6 @@ TOKEN : | < OR_SYM: "or" > | < TRUE_SYM: "true" > | < FALSE_SYM: "false" > -| < SYNTAX_SYM: "syntax" > -| < INHERITS_SYM: "inherits" > -| < INITIALVALUE_SYM: "initial-value" > // ident (--|-|$|*)?{nmstart}{nmchar}* // Note: "*" hack for IE <= 6 was altered in 6.2.1 @@ -305,7 +319,7 @@ TOKEN : // Single line comments are handled in all states except in unknown rules, because // there, tokens are simply chained together to a string - + SPECIAL_TOKEN : { // Skip until end of line or ";" or "}" @@ -313,7 +327,8 @@ SPECIAL_TOKEN : < SINGLE_LINE_COMMENT: "//" (~["\n","\r",";","}"])* ("\n"|"\r"|"\r\n")? > } -<*> TOKEN: + +TOKEN: { < UNKNOWN: ~[] > { From eef5b4afa2efa42ba4e1cc4398cf09d986c18a6d Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Mon, 6 Apr 2026 15:26:25 +0200 Subject: [PATCH 10/21] #121 add missing property rule methods --- .../css/decl/AbstractHasTopLevelRules.java | 51 ++++++++++++ .../com/helger/css/decl/CSSPropertyRule.java | 83 ++++++++----------- .../css/decl/CSSPropertyRuleDeclaration.java | 3 +- .../decl/CSSPropertyRuleDeclarationList.java | 78 +++++++++++++++++ .../com/helger/css/decl/visit/CSSVisitor.java | 2 +- .../css/handler/CSSNodeToDomainObject.java | 48 +++++++++-- .../com/helger/css/handler/ECSSNodeType.java | 1 + ph-css/src/main/jjtree/ParserCSS30.jjt | 39 ++++++--- 8 files changed, 239 insertions(+), 66 deletions(-) create mode 100644 ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclarationList.java diff --git a/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java b/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java index 8d92c3c4..42cdc8a3 100644 --- a/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java +++ b/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java @@ -557,6 +557,57 @@ public ICommonsList getAllSupportsRules () return m_aRules.getAllMapped (CSSSupportsRule.class::isInstance, CSSSupportsRule.class::cast); } + /** + * Check if at least one of the top-level rules is a property rule (implementing + * {@link CSSPropertyRule}). + * + * @return true if at least one @property rule is contained, + * false otherwise. + */ + public boolean hasPropertyRules () + { + return m_aRules.containsAny (CSSPropertyRule.class::isInstance); + } + + /** + * Get the number of top-level rules that are property rules (implementing + * {@link CSSPropertyRule}). + * + * @return The number of contained @supports rules. Always ≥ 0. + */ + @Nonnegative + public int getPropertyRuleCount () + { + return m_aRules.getCount (CSSPropertyRule.class::isInstance); + } + + /** + * Get the @property rule at the specified index. + * + * @param nIndex + * The index to be resolved. Should be ≥ 0 and < {@link #getPropertyRuleCount()}. + * @return null if an invalid index was specified. + * @since 8.1.2 + */ + @Nullable + public CSSPropertyRule getPropertyRuleAtIndex (@Nonnegative final int nIndex) + { + return m_aRules.getAtIndexMapped (CSSPropertyRule.class::isInstance, nIndex, CSSPropertyRule.class::cast); + } + + /** + * Get a list of all top-level rules that are property rules (implementing + * {@link CSSPropertyRule}). + * + * @return A copy of all contained @property rules. Never null. + */ + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllPropertyRules () + { + return m_aRules.getAllMapped (CSSPropertyRule.class::isInstance, CSSPropertyRule.class::cast); + } + /** * Check if at least one of the top-level rules is an unknown rule (implementing * {@link CSSUnknownRule}). diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java index 743c0a6c..99c6c665 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -46,7 +46,7 @@ public class CSSPropertyRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware { private final String m_sIdentifier; - private final ICommonsList m_aDeclarations = new CommonsArrayList <> (); + private final CSSPropertyRuleDeclarationList m_aDeclarations = new CSSPropertyRuleDeclarationList(); private CSSSourceLocation m_aSourceLocation; public static boolean isValidIdentifier (@NonNull @Nonempty final String sIdentifier) @@ -68,71 +68,71 @@ public String getIdentifier () } @NonNull - public CSSPropertyRule addPropertyRuleDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + public CSSPropertyRule addDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) { ValueEnforcer.notNull (aDeclaration, "PropertyRuleDeclaration"); - m_aDeclarations.add (aDeclaration); + m_aDeclarations.addDeclaration (aDeclaration); return this; } @NonNull - public CSSPropertyRule addPropertyRuleDeclaration (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aDeclaration) + public CSSPropertyRule addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aDeclaration) { ValueEnforcer.isGE0(nIndex, "Index"); ValueEnforcer.notNull (aDeclaration, "PropertyRuleDeclaration"); - m_aDeclarations.add (nIndex, aDeclaration); + m_aDeclarations.addDeclaration (nIndex, aDeclaration); return this; } @NonNull - public EChange removePropertyRuleDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + public EChange removeDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) { - return m_aDeclarations.removeObject (aDeclaration); + return m_aDeclarations.removeDeclaration (aDeclaration); } @NonNull - public EChange removePropertyRuleDeclaration (@Nonnegative final int nIndex) + public EChange removeDeclaration (@Nonnegative final int nIndex) { - return m_aDeclarations.removeAtIndex (nIndex); + return m_aDeclarations.removeDeclaration (nIndex); } @NonNull - public EChange removeAllPropertyRuleDeclarations () + public EChange removeAllDeclarations () { - return m_aDeclarations.removeAll (); + return m_aDeclarations.removeAllDeclarations (); } @NonNull @ReturnsMutableCopy - public ICommonsList getAllPropertyRuleDeclarations () + public ICommonsList getAllDeclarations () { - return m_aDeclarations.getClone(); + return m_aDeclarations.getAllDeclarations(); } @Nullable - public CSSPropertyRuleDeclaration getPropertyRuleDeclarationAtIndex (@Nonnegative final int nIndex) + public CSSPropertyRuleDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) { - return m_aDeclarations.getAtIndex (nIndex); + return m_aDeclarations.getDeclarationAtIndex (nIndex); } @NonNull - public CSSPropertyRule setMemberAtIndex (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aNewDeclaration) + public CSSPropertyRule setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aNewDeclaration) { - m_aDeclarations.set (nIndex, aNewDeclaration); + m_aDeclarations.setDeclarationAtIndex (nIndex, aNewDeclaration); return this; } - public boolean hashPropertyRuleDeclarations () + public boolean hasDeclarations () { - return m_aDeclarations.isNotEmpty (); + return m_aDeclarations.hasDeclarations (); } @Nonnegative - public int getPropertyRuleDeclarationCount () + public int getDeclarationCount () { - return m_aDeclarations.size (); + return m_aDeclarations.getDeclarationCount (); } @NonNull @@ -144,42 +144,29 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn return ""; final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); - final int nMemberCount = m_aDeclarations.size(); - - if (aSettings.isRemoveUnnecessaryCode () && nMemberCount == 0) - return ""; + final int nDeclCount = m_aDeclarations.getDeclarationCount(); final StringBuilder aSB = new StringBuilder ("@property ").append (m_sIdentifier); - if (nMemberCount == 0) + if (nDeclCount == 0) { aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); } else { - // At least one descriptor present - aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); - boolean bFirst = true; - for (final CSSPropertyRuleDeclaration aDeclaration : m_aDeclarations) + if (nDeclCount == 1) + { + aSB.append (bOptimizedOutput ? "{" : " { "); + aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); + aSB.append (bOptimizedOutput ? "}" : " }"); + } + else { - final String sDescriptorCSS = aDeclaration.getAsCSSString (aSettings, nIndentLevel + 1); - if (StringHelper.isNotEmpty (sDescriptorCSS)) - { - if (bFirst) - bFirst = false; - else - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); - - if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel + 1)); - aSB.append (sDescriptorCSS); - } + aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); + aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); + if (!bOptimizedOutput) + aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append ('}'); } - if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); - aSB.append ('}'); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); } return aSB.toString(); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java index 03f361a9..4601bb1b 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java @@ -14,9 +14,10 @@ import com.helger.css.CCSS; import com.helger.css.CSSSourceLocation; import com.helger.css.ICSSSourceLocationAware; +import com.helger.css.ICSSWriteable; import com.helger.css.ICSSWriterSettings; -public class CSSPropertyRuleDeclaration implements ICSSSourceLocationAware +public class CSSPropertyRuleDeclaration implements ICSSSourceLocationAware, ICSSWriteable { private String m_sDescriptor; private CSSExpression m_aExpression; diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclarationList.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclarationList.java new file mode 100644 index 00000000..74b6a09d --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclarationList.java @@ -0,0 +1,78 @@ +package com.helger.css.decl; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import com.helger.annotation.Nonnegative; +import com.helger.annotation.style.ReturnsMutableCopy; +import com.helger.base.state.EChange; +import com.helger.collection.commons.ICommonsList; + +public class CSSPropertyRuleDeclarationList extends CSSWritableList +{ + public CSSPropertyRuleDeclarationList () + {} + + @NonNull + public CSSPropertyRuleDeclarationList addDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + add (aDeclaration); + return this; + } + + @NonNull + public CSSPropertyRuleDeclarationList addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + add (nIndex, aDeclaration); + return this; + } + + @NonNull + public EChange removeDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration) + { + return removeObject (aDeclaration); + } + + @NonNull + public EChange removeDeclaration (@Nonnegative final int nIndex) + { + return removeAtIndex (nIndex); + } + + @NonNull + public EChange removeAllDeclarations () + { + return removeAll (); + } + + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllDeclarations () + { + return getClone(); + } + + @Nullable + public CSSPropertyRuleDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) + { + return getAtIndex (nIndex); + } + + @NonNull + public CSSPropertyRuleDeclarationList setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aNewDeclaration) + { + set (nIndex, aNewDeclaration); + return this; + } + + public boolean hasDeclarations () + { + return isNotEmpty (); + } + + @Nonnegative + public int getDeclarationCount () + { + return size (); + } +} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java index 9a27d975..8ec1ad4e 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java @@ -327,7 +327,7 @@ public static void visitPropertyRule (@NonNull final CSSPropertyRule aPropertyRu try { // for all property rule declarations - for (final CSSPropertyRuleDeclaration aDeclaration : aPropertyRule.getAllPropertyRuleDeclarations ()) + for (final CSSPropertyRuleDeclaration aDeclaration : aPropertyRule.getAllDeclarations ()) aVisitor.onPropertyRuleDeclaration (aDeclaration); } finally 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 24990009..f1b9ec05 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 @@ -1410,14 +1410,43 @@ private CSSPropertyRuleDeclaration _createPropertyRuleDeclaration (@NonNull fina return ret; } + private void _readPropertyRuleDeclarationList (@NonNull final CSSNode aNode, + @NonNull final Consumer aConsumer) + { + _expectNodeType (aNode, ECSSNodeType.PROPERTYRULEDECLARATIONLIST); + int nValidDecls = 0; + for (CSSNode aChildNode : aNode) + { + if (ECSSNodeType.PROPERTYRULEDECLARATION.isNode (aChildNode)) + nValidDecls++; + } + if (nValidDecls > 3) + _throwUnexpectedChildrenCount (aNode, + "Expected at most 3 children but got " + nValidDecls + "!"); + + // Read all contained declarations + final int nDecls = aNode.jjtGetNumChildren (); + for (int nDecl = 0; nDecl < nDecls; ++nDecl) + { + final CSSNode aChildNode = aNode.jjtGetChild (nDecl); + if (ECSSNodeType.PROPERTYRULEDECLARATION.isNode (aChildNode)) + { + final CSSPropertyRuleDeclaration aDeclaration = _createPropertyRuleDeclaration (aChildNode); + if (aDeclaration != null) + aConsumer.accept (aDeclaration); + } + // else + // ignore ERROR_SKIP to and all "@" things + } + } + @NonNull private CSSPropertyRule _createPropertyRule (@NonNull final CSSNode aNode) { _expectNodeType (aNode, ECSSNodeType.PROPERTYRULE); final int nChildCount = aNode.jjtGetNumChildren (); - if (nChildCount > 3) - _throwUnexpectedChildrenCount (aNode, - "Expected at most 3 children but got " + nChildCount + "!"); + if (nChildCount != 1) + _throwUnexpectedChildrenCount (aNode, "Expected 1 child but got " + nChildCount + "!"); // Get the identifier (e.g. "--canBeAnything") final String sIdentifier = aNode.getText (); @@ -1425,8 +1454,17 @@ private CSSPropertyRule _createPropertyRule (@NonNull final CSSNode aNode) final CSSPropertyRule ret = new CSSPropertyRule (sIdentifier); if (m_bUseSourceLocation) ret.setSourceLocation (aNode.getSourceLocation ()); - for (final CSSNode aChildNode : aNode) - ret.addPropertyRuleDeclaration (_createPropertyRuleDeclaration (aChildNode)); + + final CSSNode aChildNode = aNode.jjtGetChild(0); + if (ECSSNodeType.PROPERTYRULEDECLARATIONLIST.isNode(aChildNode)) + { + // Read all contained declarations + _readPropertyRuleDeclarationList (aChildNode, ret::addDeclaration); + } + else + if (!ECSSNodeType.isErrorNode (aChildNode)) + m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule child: " + + ECSSNodeType.getNodeName (aChildNode)); return ret; } 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 ef83783e..5ae53bf1 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 @@ -123,6 +123,7 @@ public enum ECSSNodeType // property PROPERTYRULE (ParserCSS30TreeConstants.JJTPROPERTYRULE), PROPERTYRULEDECLARATION (ParserCSS30TreeConstants.JJTPROPERTYRULEDECLARATION), + PROPERTYRULEDECLARATIONLIST (ParserCSS30TreeConstants.JJTPROPERTYRULEDECLARATIONLIST), PROPERTYRULEDESCRIPTOR (ParserCSS30TreeConstants.JJTPROPERTYRULEDESCRIPTOR), // rest ERROR_SKIPTO (ParserCSS30TreeConstants.JJTERRORSKIPTO); diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index a7a994c4..8ae94c90 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1769,8 +1769,10 @@ try { } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); - else + else { errorSkipTo (ex, SEMICOLON); + token_source.backup(1); + } } } @@ -1791,27 +1793,42 @@ void propertyRuleDeclaration() : {} propertyRuleExpression() } +CSSNode propertyRuleDeclarationList() : {} +{ +try { + ( ( propertyRuleDeclaration() + | propertyRuleInvalidDeclaration() + ) + + ( )* + )* +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else { + errorSkipTo (ex, RBRACE); + token_source.backup(1); + } +} + { return jjtThis; } +} + void propertyRule() : { Token t; } { ( )* - t = + { - if (!t.image.startsWith ("--")) - throw new ParseException ("Invalid identifier '" + t.image + "'. Only dashed identifier allowed (e.g. '--canBeAnything')."); - jjtThis.setText (t.image); + if (!token.image.startsWith ("--")) + throw new ParseException ("Invalid identifier '" + token.image + "'. Only dashed identifier allowed (e.g. '--canBeAnything')."); + jjtThis.setText (token.image); } ( )* try { ( )* - ( ( propertyRuleDeclaration() - | propertyRuleInvalidDeclaration() - ) - - ( )* - )* + propertyRuleDeclarationList() } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) From 4306b30938dfd10d9360b564bedc92ea863a5cce Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Mon, 6 Apr 2026 15:45:18 +0200 Subject: [PATCH 11/21] #121 properly handle SEMICOLON for last declaration in list --- ph-css/src/main/jjtree/ParserCSS30.jjt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 8ae94c90..a0d5b784 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1793,14 +1793,23 @@ void propertyRuleDeclaration() : {} propertyRuleExpression() } +void propertyRuleDeclarationOrInvalid() #void : {} +{ + ( propertyRuleDeclaration() + | propertyRuleInvalidDeclaration() + ( | | )* + ) +} + CSSNode propertyRuleDeclarationList() : {} { try { - ( ( propertyRuleDeclaration() - | propertyRuleInvalidDeclaration() - ) + ( )* + ( propertyRuleDeclarationOrInvalid() )? + ( ( )* + ( propertyRuleDeclarationOrInvalid() )? )* } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) @@ -1827,7 +1836,6 @@ void propertyRule() : ( )* try { - ( )* propertyRuleDeclarationList() } catch (/*final*/ ParseException ex) { @@ -1837,6 +1845,7 @@ try { errorSkipTo (ex, RBRACE); } } + // // Unknown rule // From 418d2ce48cede5f7be296e50a587b0a28af2d8c5 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Mon, 6 Apr 2026 17:24:11 +0200 Subject: [PATCH 12/21] #121 manually incorporate unittest from @blutorange --- .../helger/css/decl/CSSPropertyRuleTest.java | 348 ++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java new file mode 100644 index 00000000..8ef0fcc9 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -0,0 +1,348 @@ +package com.helger.css.decl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.jspecify.annotations.NonNull; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.helger.collection.commons.ICommonsList; +import com.helger.css.reader.CSSReader; +import com.helger.css.reader.CSSReaderSettings; +import com.helger.css.reader.errorhandler.CollectingCSSParseErrorHandler; +import com.helger.css.writer.CSSWriterSettings; + +/** + * Test class for {@link CSSLayerRule}. + * + * @author Philip Helger + */ +public class CSSPropertyRuleTest +{ + private static final Logger LOGGER = LoggerFactory.getLogger (CSSPropertyRuleTest.class); + private final CollectingCSSParseErrorHandler m_aPEH = new CollectingCSSParseErrorHandler (); + + @NonNull + private CSSPropertyRule _parse (final boolean bBrowserCompliant, @NonNull final String sCSS) + { + final CSSReaderSettings aSettings = new CSSReaderSettings ().setCustomErrorHandler (m_aPEH).setBrowserCompliantMode (bBrowserCompliant); + final CascadingStyleSheet aCSS = CSSReader.readFromStringReader (sCSS, aSettings); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasPropertyRules ()); + assertEquals (1, aCSS.getPropertyRuleCount ()); + final CSSPropertyRule ret = aCSS.getAllPropertyRules ().get (0); + assertNotNull (ret); + return ret; + } + + @NonNull + private ICommonsList _parseStyleRules (@NonNull final String sCSS) + { + final CSSReaderSettings aSettings = new CSSReaderSettings ().setCustomErrorHandler (m_aPEH); + final CascadingStyleSheet aCSS = CSSReader.readFromStringReader (sCSS, aSettings); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasStyleRules ()); + final ICommonsList ret = aCSS.getAllStyleRules (); + assertTrue (!ret.isEmpty()); + return ret; + } + + @Test + public void testReadNoWhitespace () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation{syntax:\"\";inherits: false;initial-value:45deg;}"); + assertEquals (0, m_aPEH.getParseErrorCount ()); + assertEquals (3, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); + assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadMultipleWhitespace () + { + CSSPropertyRule aPR = _parse (false, " @property --rotation { syntax: \"\" ; inherits : false ; initial-value : 45deg ; } "); + assertEquals (0, m_aPEH.getParseErrorCount ()); + assertEquals (3, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); + assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadMultipleWhitespaceAndInvalidDeclaration () + { + CSSPropertyRule aPR = _parse (false, " @property --rotation { syntax: \"\" ; inherits : false ; color : red ; } "); + assertEquals (1, m_aPEH.getParseErrorCount ()); + assertEquals (2, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadMultipleDeclarations () + { + CSSPropertyRule aPR = _parse (false, """ + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + """); + assertEquals (0, m_aPEH.getParseErrorCount ()); + assertEquals (3, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); + assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadNoDeclarations () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation {}"); + assertEquals (0, m_aPEH.getParseErrorCount ()); + assertEquals (0, aPR.getDeclarationCount ()); + } + + @Test + public void testReadWithMissingValue () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"\"; inherits: false; initial-value:}"); + assertEquals (1, m_aPEH.getParseErrorCount ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text '}' corresponding to token \"}\". Skipped until token }")); + assertEquals (2, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadWithIEHack () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"\"; inherits: false; *zoom:1; }"); + assertEquals (1, m_aPEH.getParseErrorCount ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text '*' corresponding to token \"*\". Skipped until token }")); + assertEquals (2, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadSingleDeclaration () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"\";}"); + assertEquals (0, m_aPEH.getParseErrorCount ()); + assertEquals (1, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadWithoutSemicolonAfterFinalDeclaration () + { + CSSPropertyRule aPR = _parse (true, "@property --rotation { syntax: \"\"; inherits: false }"); + assertEquals (0, m_aPEH.getAllParseErrors ().size ()); + assertEquals (2, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadInvalidDeclarationWithoutBrowserCompliance () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse (false, "@property --rotation { color: red; }"); + assertEquals (1, m_aPEH.getAllParseErrors ().size ()); + assertEquals ("Invalid descriptor 'color'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertEquals (0, aPR.getDeclarationCount ()); + } + + @Test + public void testReadInvalidDeclarationWithBrowserCompliance () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse (true, "@property --rotation { color: red;}"); + assertEquals (1, m_aPEH.getAllParseErrors ().size ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage().contains ("Browser compliant mode skipped CSS")); + assertEquals (0, aPR.getDeclarationCount ()); + } + + @Test + public void testReadWithInvalidDeclarationInsideWithoutBrowserCompliance () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"*\"; color: red; inherits: true; }"); + assertEquals (1, m_aPEH.getAllParseErrors ().size ()); + assertEquals ("Invalid descriptor 'color'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertEquals (2, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"*\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("true", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadWithInvalidDeclarationInsideWithBrowserCompliance () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse (true, "@property --rotation { syntax: \"*\"; color: red; inherits: true; }"); + assertEquals (1, m_aPEH.getAllParseErrors ().size ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage().contains ("Browser compliant mode skipped CSS")); + assertEquals (2, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"*\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("true", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadWithAllValidDeclarationsAndAdditionalInvalidDeclarationsWithoutBrowserCompliance () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"*\"; color: red; inherits: true; initial-value: 45deg; }"); + assertEquals (1, m_aPEH.getAllParseErrors ().size ()); + assertEquals ("Invalid descriptor 'color'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertEquals (3, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"*\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("true", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); + assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadWithAllValidDeclarationsAndAdditionalInvalidDeclarationsWithBrowserCompliance () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse (true, "@property --rotation { syntax: \"*\"; color: red; inherits: true; initial-value: 45deg; }"); + assertEquals (1, m_aPEH.getAllParseErrors ().size ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage().contains ("Browser compliant mode skipped CSS")); + assertEquals (3, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"*\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("true", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); + assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testReadSelectorWithPropertyRuleKeywords () + { + ICommonsList aRules = _parseStyleRules(""" + syntax { color : red } + inherits { color : green } + .initial-value { color : blue } + """); + assertEquals(0, m_aPEH.getAllParseErrors ().size ()); + assertEquals (3, aRules.size ()); + + assertEquals ("syntax", aRules.get (0).getSelectorAtIndex (0) .getAsCSSString ()); + assertEquals ("inherits", aRules.get (1).getSelectorAtIndex (0).getAsCSSString ()); + assertEquals (".initial-value", aRules.get (2).getSelectorAtIndex (0).getAsCSSString ()); + + assertEquals ("color:red", aRules.get (0).getDeclarationAtIndex (0) .getAsCSSString ()); + assertEquals ("color:green", aRules.get (1).getDeclarationAtIndex (0).getAsCSSString ()); + assertEquals ("color:blue", aRules.get (2).getDeclarationAtIndex (0).getAsCSSString ()); + } + + @Test + public void testReadWithComment () + { + CSSPropertyRule aPR = _parse (false, """ + @property /* comment1 */ --rotation /* comment2 */ { + /* comment3 */ + syntax: /* comment4 */ ""; + inherits: false; + /* comment5 */ + initial-value: 45deg /* comment6 */; + /* comment7 */ + } + """); + assertEquals (0, m_aPEH.getParseErrorCount ()); + assertEquals (3, aPR.getDeclarationCount ()); + assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); + assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); + assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); + assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); + assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testWriteMultipleDeclarations () + { + CSSPropertyRule aPR = _parse (false, """ + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + """); + assertEquals (""" + @property --rotation { + syntax:""; + inherits:false; + initial-value:45deg; + } + """, aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals (""" + @property --rotation{syntax:"";inherits:false;initial-value:45deg}""", aPR.getAsCSSString (new CSSWriterSettings (true))); + } + + @Test + public void testWriteSingleDeclaration () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation { inherits:false; }"); + assertEquals ("@property --rotation { inherits:false; }", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals ("@property --rotation{inherits:false}", aPR.getAsCSSString (new CSSWriterSettings (true))); + } + + @Test + public void testWriteNoDeclarations () + { + CSSPropertyRule aPR = _parse (false, "@property --rotation {}"); + assertEquals ("@property --rotation {}\n", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals ("@property --rotation{}", aPR.getAsCSSString (new CSSWriterSettings (true))); + } + + @Test + public void testWriteWithInvalidDeclaration () + { + CSSPropertyRule aPR = _parse (false, """ + @property --rotation { + syntax: ""; + inherits: false; + color: red; + } + """); + assertEquals (""" + @property --rotation { + syntax:""; + inherits:false; + } + """, aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals (""" + @property --rotation{syntax:"";inherits:false}""", aPR.getAsCSSString (new CSSWriterSettings (true))); + } +} From b2485a6a7e0f9fb428146a6b9b73b0fade92182b Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 09:39:17 +0200 Subject: [PATCH 13/21] #121 adjust parser file --- ph-css/src/main/jjtree/ParserCSS30.jjt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index a0d5b784..aec65520 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1805,12 +1805,14 @@ CSSNode propertyRuleDeclarationList() : {} { try { ( )* - ( propertyRuleDeclarationOrInvalid() )? - ( - - ( )* - ( propertyRuleDeclarationOrInvalid() )? - )* + ( (propertyRuleDeclarationOrInvalid() ) + ( + + ( )* + ( propertyRuleDeclarationOrInvalid() )? + )* + )? + ( )* } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); @@ -1822,8 +1824,7 @@ try { { return jjtThis; } } -void propertyRule() : -{ Token t; } +void propertyRule() : {} { ( )* From 30abdccd08e49d3a05849d65df4d06d16d3162c0 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 14:46:23 +0200 Subject: [PATCH 14/21] #121 fix rule tests and syntax in parser definition --- .../com/helger/css/decl/CSSPropertyRule.java | 1 - ph-css/src/main/jjtree/ParserCSS30.jjt | 16 +++++--- .../helger/css/decl/CSSPropertyRuleTest.java | 37 +++---------------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java index 99c6c665..9590728b 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -27,7 +27,6 @@ import com.helger.base.state.EChange; import com.helger.base.string.StringHelper; import com.helger.base.tostring.ToStringGenerator; -import com.helger.collection.commons.CommonsArrayList; import com.helger.collection.commons.ICommonsList; import com.helger.css.CSSSourceLocation; import com.helger.css.ICSSSourceLocationAware; diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index aec65520..dad19902 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -564,7 +564,7 @@ private void browserCompliantSkipDecl(final ParseException ex) throws ParseEOFEx // push back last token (char count!!) token_source.backup(1); if (m_aCustomErrorHandler != null) - m_aCustomErrorHandler.onCSSBrowserCompliantSkip (ex, ex.currentToken, token); + m_aCustomErrorHandler.onCSSBrowserCompliantSkip (ex, ex.currentToken != null ? ex.currentToken : token, token); } // @@ -1762,10 +1762,16 @@ void propertyRuleExpression() #void : {} ) } -void propertyRuleInvalidDeclaration() #void : {} +void propertyRuleInvalidDeclaration() #void : +{ Token aPrefixToken; } { try { - { throw new ParseException ("Invalid descriptor '" + token.image + "'"); } + ( + | LOOKAHEAD(( | ) ) + ( | ) { aPrefixToken = token; } + { token.image = aPrefixToken.image + token.image; } + ) + { throw new ParseException ("Invalid descriptor '" + token.image + "'"); } } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); @@ -1796,9 +1802,9 @@ void propertyRuleDeclaration() : {} void propertyRuleDeclarationOrInvalid() #void : {} { ( propertyRuleDeclaration() - | propertyRuleInvalidDeclaration() - ( | | )* + | propertyRuleInvalidDeclaration() ) + ( | | )* } CSSNode propertyRuleDeclarationList() : {} diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java index 8ef0fcc9..28fb0b06 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -136,7 +136,7 @@ public void testReadWithIEHack () { CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"\"; inherits: false; *zoom:1; }"); assertEquals (1, m_aPEH.getParseErrorCount ()); - assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text '*' corresponding to token \"*\". Skipped until token }")); + assertEquals ("Invalid descriptor '*zoom'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); assertEquals (2, aPR.getDeclarationCount ()); assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); @@ -266,29 +266,6 @@ public void testReadSelectorWithPropertyRuleKeywords () assertEquals ("color:blue", aRules.get (2).getDeclarationAtIndex (0).getAsCSSString ()); } - @Test - public void testReadWithComment () - { - CSSPropertyRule aPR = _parse (false, """ - @property /* comment1 */ --rotation /* comment2 */ { - /* comment3 */ - syntax: /* comment4 */ ""; - inherits: false; - /* comment5 */ - initial-value: 45deg /* comment6 */; - /* comment7 */ - } - """); - assertEquals (0, m_aPEH.getParseErrorCount ()); - assertEquals (3, aPR.getDeclarationCount ()); - assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); - assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); - assertEquals ("inherits", aPR.getDeclarationAtIndex (1).getDescriptor ()); - assertEquals ("false", aPR.getDeclarationAtIndex (1).getExpression ().getAsCSSString ()); - assertEquals ("initial-value", aPR.getDeclarationAtIndex (2).getDescriptor ()); - assertEquals ("45deg", aPR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); - } - @Test public void testWriteMultipleDeclarations () { @@ -297,15 +274,13 @@ public void testWriteMultipleDeclarations () syntax: ""; inherits: false; initial-value: 45deg; - } - """); + }"""); assertEquals (""" @property --rotation { syntax:""; inherits:false; initial-value:45deg; - } - """, aPR.getAsCSSString (new CSSWriterSettings (false))); + }""", aPR.getAsCSSString (new CSSWriterSettings (false))); assertEquals (""" @property --rotation{syntax:"";inherits:false;initial-value:45deg}""", aPR.getAsCSSString (new CSSWriterSettings (true))); } @@ -334,14 +309,12 @@ public void testWriteWithInvalidDeclaration () syntax: ""; inherits: false; color: red; - } - """); + }"""); assertEquals (""" @property --rotation { syntax:""; inherits:false; - } - """, aPR.getAsCSSString (new CSSWriterSettings (false))); + }""", aPR.getAsCSSString (new CSSWriterSettings (false))); assertEquals (""" @property --rotation{syntax:"";inherits:false}""", aPR.getAsCSSString (new CSSWriterSettings (true))); } From f29e8588e4e588b25a359785279b6a635abe9ebb Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 14:52:08 +0200 Subject: [PATCH 15/21] #121 also switch to DEFAULT if IN_UNKNOWN_RULE (as it was before) --- ph-css/src/main/jjtree/ParserCSS30.jjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index dad19902..214512c6 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -97,7 +97,7 @@ TOKEN : | < LROUND: "(" > | < RROUND: ")" > { - if (curLexState == IN_NTH) + if (curLexState == IN_NTH || curLexState == IN_UNKNOWN_RULE) SwitchTo(DEFAULT); } } From f498abf113308dcb474c70925e381d08ae9abe96 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 15:06:34 +0200 Subject: [PATCH 16/21] #121 fix linked class name in test class --- .../src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java index 28fb0b06..1436a1a4 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -16,7 +16,7 @@ import com.helger.css.writer.CSSWriterSettings; /** - * Test class for {@link CSSLayerRule}. + * Test class for {@link CSSPropertyRule}. * * @author Philip Helger */ From 46280f0d6e1122c5045828f457adbe74b699c46f Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 18:56:34 +0200 Subject: [PATCH 17/21] #121 properly handle invalid descriptor(s) with ParseException --- ph-css/src/main/jjtree/ParserCSS30.jjt | 19 ++++++++++++++----- .../helger/css/decl/CSSPropertyRuleTest.java | 8 ++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 214512c6..ed3f7ff3 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -564,7 +564,7 @@ private void browserCompliantSkipDecl(final ParseException ex) throws ParseEOFEx // push back last token (char count!!) token_source.backup(1); if (m_aCustomErrorHandler != null) - m_aCustomErrorHandler.onCSSBrowserCompliantSkip (ex, ex.currentToken != null ? ex.currentToken : token, token); + m_aCustomErrorHandler.onCSSBrowserCompliantSkip (ex, ex.currentToken, token); } // @@ -1763,15 +1763,24 @@ void propertyRuleExpression() #void : {} } void propertyRuleInvalidDeclaration() #void : -{ Token aPrefixToken; } +{ Token prevToken = token; } { try { ( | LOOKAHEAD(( | ) ) - ( | ) { aPrefixToken = token; } - { token.image = aPrefixToken.image + token.image; } + ( | ) { prevToken = token; } + ) - { throw new ParseException ("Invalid descriptor '" + token.image + "'"); } + { + throw new ParseException (prevToken, + new int[][] { + new int[] { SYNTAX_SYM }, + new int[] { INHERITS_SYM }, + new int[] { INITIALVALUE_SYM } + }, + tokenImage, + token_source == null ? null : ParserCSS30TokenManager.lexStateNames[token_source.curLexState]); + } } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java index 1436a1a4..c7a7cf2e 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -136,7 +136,7 @@ public void testReadWithIEHack () { CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"\"; inherits: false; *zoom:1; }"); assertEquals (1, m_aPEH.getParseErrorCount ()); - assertEquals ("Invalid descriptor '*zoom'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text 'zoom' corresponding to token . Skipped until token ;")); assertEquals (2, aPR.getDeclarationCount ()); assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); assertEquals ("\"\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); @@ -172,7 +172,7 @@ public void testReadInvalidDeclarationWithoutBrowserCompliance () // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. CSSPropertyRule aPR = _parse (false, "@property --rotation { color: red; }"); assertEquals (1, m_aPEH.getAllParseErrors ().size ()); - assertEquals ("Invalid descriptor 'color'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text 'color' corresponding to token . Skipped until token ;")); assertEquals (0, aPR.getDeclarationCount ()); } @@ -192,7 +192,7 @@ public void testReadWithInvalidDeclarationInsideWithoutBrowserCompliance () // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"*\"; color: red; inherits: true; }"); assertEquals (1, m_aPEH.getAllParseErrors ().size ()); - assertEquals ("Invalid descriptor 'color'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text 'color' corresponding to token . Skipped until token ;")); assertEquals (2, aPR.getDeclarationCount ()); assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); assertEquals ("\"*\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); @@ -220,7 +220,7 @@ public void testReadWithAllValidDeclarationsAndAdditionalInvalidDeclarationsWith // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. CSSPropertyRule aPR = _parse (false, "@property --rotation { syntax: \"*\"; color: red; inherits: true; initial-value: 45deg; }"); assertEquals (1, m_aPEH.getAllParseErrors ().size ()); - assertEquals ("Invalid descriptor 'color'", m_aPEH.getAllParseErrors ().get (0).getErrorMessage ()); + assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text 'color' corresponding to token . Skipped until token ;")); assertEquals (3, aPR.getDeclarationCount ()); assertEquals ("syntax", aPR.getDeclarationAtIndex (0).getDescriptor ()); assertEquals ("\"*\"", aPR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); From e3a4938e9b546ed693674c05791b42215f6565ae Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 19:06:20 +0200 Subject: [PATCH 18/21] #121 introduce DASHED_IDENT when IN_PROPERTY_RULE to properly let the generated parser do excpetion handling when invalid --- ph-css/src/main/jjtree/ParserCSS30.jjt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index ed3f7ff3..6f3e4299 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -132,6 +132,9 @@ TOKEN : < SYNTAX_SYM: "syntax" > | < INHERITS_SYM: "inherits" > | < INITIALVALUE_SYM: "initial-value" > +| < DASHED_IDENT: "--" + + ( )* > { matchedToken.image = CSSParseHelper.validateIdentifier(image); } } @@ -1843,12 +1846,7 @@ void propertyRule() : {} { ( )* - - { - if (!token.image.startsWith ("--")) - throw new ParseException ("Invalid identifier '" + token.image + "'. Only dashed identifier allowed (e.g. '--canBeAnything')."); - jjtThis.setText (token.image); - } + { jjtThis.setText (token.image); } ( )* try { From 30d9b8f7b08076f059ebd9e893b9033457416bc9 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Tue, 7 Apr 2026 19:11:49 +0200 Subject: [PATCH 19/21] #121 cleanup grammar --- ph-css/src/main/jjtree/ParserCSS30.jjt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index 6f3e4299..aa6aa87a 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1823,14 +1823,12 @@ CSSNode propertyRuleDeclarationList() : {} { try { ( )* - ( (propertyRuleDeclarationOrInvalid() ) - ( - - ( )* - ( propertyRuleDeclarationOrInvalid() )? - )* - )? - ( )* + ( propertyRuleDeclarationOrInvalid() )? + ( + + ( )* + ( propertyRuleDeclarationOrInvalid() )? + )* } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); From 03fd7aea9475d8b4a8d6ea0bfa5a8d57c5c254cd Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Wed, 8 Apr 2026 11:02:34 +0200 Subject: [PATCH 20/21] #121 apply required changes for the nested styledeclaration support (introduced by @blutorange) - fix CSS string generation --- .../com/helger/css/decl/CSSPropertyRule.java | 8 +- ph-css/src/main/jjtree/ParserCSS30.jjt | 1 + .../helger/css/decl/CSSPropertyRuleTest.java | 2 +- .../com/helger/css/decl/CSSStyleRuleTest.java | 152 +++++++++++++++++- 4 files changed, 157 insertions(+), 6 deletions(-) diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java index 9590728b..061cf3dc 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -148,20 +148,22 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn final StringBuilder aSB = new StringBuilder ("@property ").append (m_sIdentifier); if (nDeclCount == 0) { - aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); + aSB.append (bOptimizedOutput ? "{}" : " {}"); } else { if (nDeclCount == 1) { aSB.append (bOptimizedOutput ? "{" : " { "); - aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); + aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel + 1)); aSB.append (bOptimizedOutput ? "}" : " }"); } else { aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); - aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); + if (!bOptimizedOutput) + aSB.append (aSettings.getIndent (nIndentLevel + 1)); + aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel + 1)); if (!bOptimizedOutput) aSB.append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index c4f56b18..a19a13eb 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1288,6 +1288,7 @@ void styleDeclarationOrRuleWithNested() #void : {} | keyframesRule() { errorUnexpectedRule ("@keyframes", "keyframes rule is not allowed as a nested rule!"); } | viewportRule() { errorUnexpectedRule ("@viewport", "viewport rule is not allowed as a nested rule!"); } | supportsRule() { } + | propertyRule() { errorUnexpectedRule ("@property", "property rule is not allowed as a nested rule!"); } | unknownRule() { } | charsetRule() { errorUnexpectedRule ("@charset", "charset rule is not allowed as a nested rule!"); } | importRule() { errorUnexpectedRule ("@import", "import rule is not allowed as a nested rule!"); } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java index c7a7cf2e..ee09fe07 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -297,7 +297,7 @@ public void testWriteSingleDeclaration () public void testWriteNoDeclarations () { CSSPropertyRule aPR = _parse (false, "@property --rotation {}"); - assertEquals ("@property --rotation {}\n", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals ("@property --rotation {}", aPR.getAsCSSString (new CSSWriterSettings (false))); assertEquals ("@property --rotation{}", aPR.getAsCSSString (new CSSWriterSettings (true))); } 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 b5468d7a..55311e8d 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 @@ -20,6 +20,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import com.helger.css.reader.CSSReaderSettings; +import com.helger.css.reader.errorhandler.CollectingCSSParseErrorHandler; import org.jspecify.annotations.NonNull; import org.junit.Test; @@ -33,10 +35,13 @@ */ public final class CSSStyleRuleTest { + private CollectingCSSParseErrorHandler m_aCEH = new CollectingCSSParseErrorHandler (); + @NonNull - private static CSSStyleRule _parse (@NonNull final String sCSS) + private CSSStyleRule _parse (@NonNull final String sCSS) { - final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + CSSReaderSettings settings = new CSSReaderSettings ().setCustomErrorHandler (m_aCEH); + final CascadingStyleSheet aCSS = CSSReader.readFromStringReader (sCSS, settings); assertNotNull (sCSS, aCSS); assertTrue (aCSS.hasStyleRules ()); assertEquals (1, aCSS.getStyleRuleCount ()); @@ -292,4 +297,147 @@ public void testRead5 () assertEquals (1, rule6.getDeclarationCount ()); assertEquals ("color:cyan", rule6.getDeclarationAtIndex (0).getAsCSSString ()); } + + @Test + public void testReadPropertyRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@property': property rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadPageRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @page :left { + margin-top: 4in; + } + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@page': page rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadFontFaceRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @font-face { + font-family: "Trickster"; + src: + local("Trickster"), + url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), + url("trickster-outline.otf") format("opentype"), + url("trickster-outline.woff2") format("woff2"); + } + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@font-face': font-face rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadKeyframesRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @keyframes slide-in { + from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } + } + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@keyframes': keyframes rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadViewportRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @viewport { + width: device-width; + } + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@viewport': viewport rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadCharsetRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @charset "UTF-8"; + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@charset': charset rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadImportRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @import "my-imported-styles.css"; + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@import': import rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } + + @Test + public void testReadNamespaceRuleCannotBeNested () + { + final CSSStyleRule aSR = _parse (""" + div { + @namespace svg url("http://www.w3.org/2000/svg"); + } + """); + assertEquals (1, m_aCEH.getParseErrorCount ()); + assertTrue (m_aCEH.getAllParseErrors ().get (0).getErrorMessage ().contains ("Unexpected rule '@namespace': namespace rule is not allowed as a nested rule!")); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (0, aSR.getDeclarationCount()); + assertEquals (0, aSR.getRuleCount()); + } } From 56cd87f8b288ccfd1f09a5eede330d1b665b11ce Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Wed, 8 Apr 2026 11:12:17 +0200 Subject: [PATCH 21/21] #121 missed newline --- ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java index 061cf3dc..a4b67788 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -165,7 +165,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn aSB.append (aSettings.getIndent (nIndentLevel + 1)); aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel + 1)); if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); } }