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 419b5752..a39ec5cd 100644
--- a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java
+++ b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java
@@ -128,6 +128,11 @@ public interface ICSSWriterSettings
*/
boolean isWriteSupportsRules ();
+ /**
+ * @return true if {@link CSSPropertyRule @property rules} should be written, false if not
+ */
+ boolean isWritePropertyRules ();
+
/**
* @return true if {@link CSSUnknownRule unknown @ rules} should be written, false if not
*/
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 bc0c9bc9..afad322a 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
@@ -609,6 +609,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
new file mode 100644
index 00000000..a4b67788
--- /dev/null
+++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java
@@ -0,0 +1,211 @@
+/*
+ * 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.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 CSSPropertyRuleDeclarationList m_aDeclarations = new CSSPropertyRuleDeclarationList();
+ 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 addDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration)
+ {
+ ValueEnforcer.notNull (aDeclaration, "PropertyRuleDeclaration");
+
+ m_aDeclarations.addDeclaration (aDeclaration);
+ return this;
+ }
+
+ @NonNull
+ public CSSPropertyRule addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aDeclaration)
+ {
+ ValueEnforcer.isGE0(nIndex, "Index");
+ ValueEnforcer.notNull (aDeclaration, "PropertyRuleDeclaration");
+
+ m_aDeclarations.addDeclaration (nIndex, aDeclaration);
+ return this;
+ }
+
+ @NonNull
+ public EChange removeDeclaration (@NonNull final CSSPropertyRuleDeclaration aDeclaration)
+ {
+ return m_aDeclarations.removeDeclaration (aDeclaration);
+ }
+
+ @NonNull
+ public EChange removeDeclaration (@Nonnegative final int nIndex)
+ {
+ return m_aDeclarations.removeDeclaration (nIndex);
+ }
+
+ @NonNull
+ public EChange removeAllDeclarations ()
+ {
+ return m_aDeclarations.removeAllDeclarations ();
+ }
+
+ @NonNull
+ @ReturnsMutableCopy
+ public ICommonsList getAllDeclarations ()
+ {
+ return m_aDeclarations.getAllDeclarations();
+ }
+
+ @Nullable
+ public CSSPropertyRuleDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex)
+ {
+ return m_aDeclarations.getDeclarationAtIndex (nIndex);
+ }
+
+ @NonNull
+ public CSSPropertyRule setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNull final CSSPropertyRuleDeclaration aNewDeclaration)
+ {
+ m_aDeclarations.setDeclarationAtIndex (nIndex, aNewDeclaration);
+ return this;
+ }
+
+ public boolean hasDeclarations ()
+ {
+ return m_aDeclarations.hasDeclarations ();
+ }
+
+ @Nonnegative
+ public int getDeclarationCount ()
+ {
+ return m_aDeclarations.getDeclarationCount ();
+ }
+
+ @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 nDeclCount = m_aDeclarations.getDeclarationCount();
+
+ final StringBuilder aSB = new StringBuilder ("@property ").append (m_sIdentifier);
+ if (nDeclCount == 0)
+ {
+ aSB.append (bOptimizedOutput ? "{}" : " {}");
+ }
+ else
+ {
+ if (nDeclCount == 1)
+ {
+ aSB.append (bOptimizedOutput ? "{" : " { ");
+ aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel + 1));
+ aSB.append (bOptimizedOutput ? "}" : " }");
+ }
+ else
+ {
+ aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ());
+ if (!bOptimizedOutput)
+ aSB.append (aSettings.getIndent (nIndentLevel + 1));
+ aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel + 1));
+ if (!bOptimizedOutput)
+ aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel));
+ aSB.append ('}');
+ }
+ }
+ 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..4601bb1b
--- /dev/null
+++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRuleDeclaration.java
@@ -0,0 +1,112 @@
+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.ICSSWriteable;
+import com.helger.css.ICSSWriterSettings;
+
+public class CSSPropertyRuleDeclaration implements ICSSSourceLocationAware, ICSSWriteable
+{
+ 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/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 9223c886..7e418645 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
@@ -37,6 +37,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;
@@ -333,6 +335,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.getAllDeclarations ())
+ aVisitor.onPropertyRuleDeclaration (aDeclaration);
+ }
+ finally
+ {
+ aVisitor.onEndPropertyRule (aPropertyRule);
+ }
+ }
+
/**
* Visit all elements of a single nested declarations.
*
@@ -414,12 +439,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 1feeb669..b8477650 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,11 +250,23 @@ public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule)
m_aTopLevelRule.pop();
}
- public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) {
+ public void onBeginPropertyRule (@NonNull final CSSPropertyRule aPropertyRule)
+ {
+ m_aTopLevelRule.push(aPropertyRule);
+ }
+
+ public void onEndPropertyRule (@NonNull final CSSPropertyRule aPropertyRule)
+ {
+ m_aTopLevelRule.pop();
+ }
+
+ public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations)
+ {
// no action
}
- public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) {
+ public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations)
+ {
// 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 0380c78d..f638106c 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
@@ -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;
@@ -65,6 +67,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)
{}
@@ -157,6 +163,14 @@ public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDecl
public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations)
{}
+ @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 3d5faaae..289b8576 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
@@ -32,6 +32,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;
@@ -74,6 +76,9 @@ public interface ICSSVisitor
*/
void onDeclaration (@NonNull CSSDeclaration aDeclaration);
+
+ void onPropertyRuleDeclaration(@NonNull CSSPropertyRuleDeclaration aDeclaration);
+
// style rules:
/**
* Called when a style rule starts.
@@ -263,6 +268,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);
+
/**
* Called when a nested declarations rule starts.
* @param aNestedDeclarations
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 a36c73fa..49241878 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
@@ -1476,6 +1476,92 @@ private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode, final
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;
+ }
+
+ 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 != 1)
+ _throwUnexpectedChildrenCount (aNode, "Expected 1 child 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 ());
+
+ 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;
+ }
+
@NonNull
private CSSUnknownRule _createUnknownRule (@NonNull final CSSNode aNode)
{
@@ -1549,30 +1635,33 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a
if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode))
ret.addRule (_createSupportsRule (aChildNode, true));
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 004604fb..01f7cc24 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
@@ -122,6 +122,11 @@ public enum ECSSNodeType
SUPPORTSCONDITIONOPERATOR (ParserCSS30TreeConstants.JJTSUPPORTSCONDITIONOPERATOR),
SUPPORTSNEGATION (ParserCSS30TreeConstants.JJTSUPPORTSNEGATION),
SUPPORTSCONDITIONINPARENS (ParserCSS30TreeConstants.JJTSUPPORTSCONDITIONINPARENS),
+ // 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/java/com/helger/css/writer/CSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java
index 0168e4e2..5619c1a6 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
@@ -69,6 +69,8 @@ public class CSSWriterSettings implements ICSSWriterSettings, ICloneable : DEFAULT
}
-
+
+TOKEN :
+{
+ < PLUS: "+" >
+| < MINUS: "-" >
+| < LROUND: "(" >
+| < RROUND: ")" >
+ {
+ if (curLexState == IN_NTH || curLexState == IN_UNKNOWN_RULE)
+ SwitchTo(DEFAULT);
+ }
+}
+
+
+TOKEN :
+{
+ < LBRACE: "{" >
+| < RBRACE: "}" > : DEFAULT
+}
+
+
TOKEN :
{
< S: (" "|"\t"|"\n"|"\r\n"|"\r"|"\f")+ >
@@ -106,16 +126,18 @@ TOKEN :
| < INTEGER: ( ["0"-"9"] )+ >
}
-
+
TOKEN :
{
- < PLUS: "+" >
-| < MINUS: "-" >
-| < LROUND: "(" >
-| < RROUND: ")" > : DEFAULT
+ < SYNTAX_SYM: "syntax" >
+| < INHERITS_SYM: "inherits" >
+| < INITIALVALUE_SYM: "initial-value" >
+| < DASHED_IDENT: "--"
+
+ ( )* > { matchedToken.image = CSSParseHelper.validateIdentifier(image); }
}
-
+
TOKEN :
{
// private reg ex come first (no actions possible!)
@@ -187,8 +209,6 @@ TOKEN :
>
| < NUMBER: >
-| < LBRACE: "{" >
-| < RBRACE: "}" >
| < LSQUARE: "[" >
| < RSQUARE: "]" >
| < DOT: "." >
@@ -241,6 +261,7 @@ TOKEN :
| < VIEWPORT_SYM: "@-" "-viewport"
| "@viewport" >
| < SUPPORTS_SYM: "@supports" >
+| < PROPERTY_SYM: "@property" > : IN_PROPERTY_RULE
| < AT_UNKNOWN: "@"
| "@" > : IN_UNKNOWN_RULE
| < IMPORTANT_SYM: "!" ( )* "important" >
@@ -253,6 +274,8 @@ TOKEN :
| < FROM_SYM: "from" >
| < TO_SYM: "to" >
| < OR_SYM: "or" >
+| < TRUE_SYM: "true" >
+| < FALSE_SYM: "false" >
// ident (--|-|$|*)?{nmstart}{nmchar}*
// Note: "*" hack for IE <= 6 was altered in 6.2.1
@@ -300,7 +323,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 "}"
@@ -308,7 +331,8 @@ SPECIAL_TOKEN :
< SINGLE_LINE_COMMENT: "//" (~["\n","\r",";","}"])* ("\n"|"\r"|"\r\n")? >
}
-<*> TOKEN:
+
+TOKEN:
{
< UNKNOWN: ~[] >
{
@@ -574,6 +598,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!"); }
@@ -652,6 +677,14 @@ String anyIdentifier() #void : {}
{ return token.image; }
}
+String booleanOperator() #void : {}
+{
+ (
+ |
+ )
+ { return token.image; }
+}
+
void url() : {}
{
{ jjtThis.setText (token.image); }
@@ -1233,6 +1266,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!"); }
@@ -1254,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!"); }
@@ -1424,6 +1459,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!"); }
)
( )*
)+
@@ -1571,6 +1607,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!"); }
)
( )*
)+
@@ -1738,6 +1775,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!"); }
)
}
@@ -1768,6 +1806,123 @@ 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 propertyRuleInvalidDeclaration() #void :
+{ Token prevToken = token; }
+{
+try {
+ (
+ | LOOKAHEAD(( | ) )
+ ( | ) { prevToken = token; }
+
+ )
+ {
+ 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);
+ else {
+ errorSkipTo (ex, SEMICOLON);
+ token_source.backup(1);
+ }
+}
+}
+
+void propertyRuleDescriptor() : {}
+{
+ ( { jjtThis.setText (token.image); }
+ | { jjtThis.setText (token.image); }
+ | { jjtThis.setText (token.image); }
+ )
+ ( )*
+}
+
+void propertyRuleDeclaration() : {}
+{
+ propertyRuleDescriptor()
+
+ ( )*
+ propertyRuleExpression()
+}
+
+void propertyRuleDeclarationOrInvalid() #void : {}
+{
+ ( propertyRuleDeclaration()
+ | propertyRuleInvalidDeclaration()
+ )
+ ( | | )*
+}
+
+CSSNode propertyRuleDeclarationList() : {}
+{
+try {
+ ( )*
+ ( propertyRuleDeclarationOrInvalid() )?
+ (
+
+ ( )*
+ ( propertyRuleDeclarationOrInvalid() )?
+ )*
+} catch (/*final*/ ParseException ex) {
+ if (m_bBrowserCompliantMode)
+ browserCompliantSkipDecl (ex);
+ else {
+ errorSkipTo (ex, RBRACE);
+ token_source.backup(1);
+ }
+}
+ { return jjtThis; }
+}
+
+void propertyRule() : {}
+{
+
+ ( )*
+ { jjtThis.setText (token.image); }
+ ( )*
+
+try {
+ propertyRuleDeclarationList()
+
+} catch (/*final*/ ParseException ex) {
+ if (m_bBrowserCompliantMode)
+ browserCompliantSkipInRule (ex);
+ else
+ errorSkipTo (ex, RBRACE);
+}
+}
+
//
// Unknown 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
new file mode 100644
index 00000000..ee09fe07
--- /dev/null
+++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java
@@ -0,0 +1,321 @@
+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 CSSPropertyRule}.
+ *
+ * @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 'zoom' 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 ());
+ assertTrue (m_aPEH.getAllParseErrors ().get (0).getErrorMessage ().contains("Encountered text 'color' corresponding to token . Skipped until token ;"));
+ 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 ());
+ 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 ());
+ 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 ());
+ 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 ());
+ 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 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 {}", 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)));
+ }
+}
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());
+ }
}