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()); + } }