diff --git a/.gitignore b/.gitignore index 33adcb10..110e61ab 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ target/ zz *.iml +.idea \ No newline at end of file diff --git a/ph-css/src/main/java/com/helger/css/ICSSWriteable.java b/ph-css/src/main/java/com/helger/css/ICSSWriteable.java index d50bf51e..fac59ec6 100644 --- a/ph-css/src/main/java/com/helger/css/ICSSWriteable.java +++ b/ph-css/src/main/java/com/helger/css/ICSSWriteable.java @@ -32,6 +32,9 @@ public interface ICSSWriteable * Get the contents of this object as a serialized CSS string for writing to * an output using the default writer settings. * + *

The general contract is that this method writes only the content of this object, but not any surrounding context. + * In particular, this method does not add any leading or trailing space or newlines. + * * @return The content of this object as CSS string. Never null. * @see #getAsCSSString(ICSSWriterSettings, int) * @since 6.0.0 @@ -46,6 +49,9 @@ default String getAsCSSString () * Get the contents of this object as a serialized CSS string for writing to * an output. * + *

The general contract is that this method writes only the content of this object, but not any surrounding context. + * In particular, this method does not add any leading or trailing space or newlines. + * * @param aSettings * The settings to be used to format the output. May not be * null. @@ -63,6 +69,9 @@ default String getAsCSSString (@NonNull final ICSSWriterSettings aSettings) * Get the contents of this object as a serialized CSS string for writing to * an output. * + *

The general contract is that this method writes only the content of this object, but not any surrounding context. + * In particular, this method does not add any leading or trailing space or newlines. + * * @param aSettings * The settings to be used to format the output. May not be * null. diff --git a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java index 88e46c41..93406497 100644 --- a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java +++ b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java @@ -16,6 +16,17 @@ */ package com.helger.css; +import com.helger.css.decl.CSSFontFaceRule; +import com.helger.css.decl.CSSKeyframesRule; +import com.helger.css.decl.CSSLayerRule; +import com.helger.css.decl.CSSMediaRule; +import com.helger.css.decl.CSSNamespaceRule; +import com.helger.css.decl.CSSNestedDeclarations; +import com.helger.css.decl.CSSPageRule; +import com.helger.css.decl.CSSPropertyRule; +import com.helger.css.decl.CSSSupportsRule; +import com.helger.css.decl.CSSUnknownRule; +import com.helger.css.decl.CSSViewportRule; import org.jspecify.annotations.NonNull; import com.helger.annotation.Nonempty; @@ -73,42 +84,58 @@ public interface ICSSWriterSettings boolean isQuoteURLs (); /** - * @return true if @namespace rules should be written, false if not + * @return true if {@link CSSNamespaceRule @namespace rules} should be written, false if not */ boolean isWriteNamespaceRules (); /** - * @return true if @font-face rules should be written, false if not + * @return true if {@link CSSNestedDeclarations nested declarations} should be written, + * false if not + */ + boolean isWriteNestedDeclarations(); + + /** + * @return true if {@link CSSFontFaceRule @font-face rules} should be written, false if not */ boolean isWriteFontFaceRules (); /** - * @return true if @keyframes rules should be written, false if not + * @return true if {@link CSSKeyframesRule @keyframes rules} should be written, false if not */ boolean isWriteKeyframesRules (); /** - * @return true if @media rules should be written, false if not + * @return true if {@link CSSLayerRule @layer rules} should be written, false if not + */ + boolean isWriteLayerRules (); + + /** + * @return true if {@link CSSMediaRule @media rules} should be written, false if not */ boolean isWriteMediaRules (); /** - * @return true if @page rules should be written, false if not + * @return true if {@link CSSPageRule @page rules} should be written, false if not */ boolean isWritePageRules (); /** - * @return true if @viewport rules should be written, false if not + * @return true if {@link CSSPropertyRule @property rules} should be written, false if not + */ + boolean isWritePropertyRules (); + + /** + * @return true if {@link CSSViewportRule @viewport rules} should be written, false if not */ boolean isWriteViewportRules (); /** - * @return true if @supports rules should be written, false if not + * @return true if {@link CSSSupportsRule @supports rules} should be written, false if not */ boolean isWriteSupportsRules (); /** - * @return true if unknown @ rules should be written, false if not + * @return true if {@link CSSUnknownRule unknown @ rules} should be written, false if not */ boolean isWriteUnknownRules (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java b/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java index 8d92c3c4..8575c4e8 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 @@ -207,6 +207,110 @@ public ICommonsList getAllRules (@NonNull final Predicate true if at least one layer rule is contained, false + * otherwise. + * @since 8.2.0 + */ + public boolean hasLayerRules () + { + return m_aRules.containsAny (CSSLayerRule.class::isInstance); + } + + /** + * Get the number of top-level rules that are layer rules (implementing {@link CSSLayerRule}). + * + * @return The number of contained layer rules. Always ≥ 0. + * @since 8.2.0 + */ + @Nonnegative + public int getLayerRuleCount () + { + return m_aRules.getCount (CSSLayerRule.class::isInstance); + } + + /** + * Get the layer rule at the specified index. + * + * @param nIndex + * The index to be resolved. Should be ≥ 0 and < {@link #getStyleRuleCount()}. + * @return The layer rule at the given index, or null if an invalid index was specified. + * @since 8.2.0 + */ + @Nullable + public CSSLayerRule getLayerRuleAtIndex (@Nonnegative final int nIndex) + { + return m_aRules.getAtIndexMapped (CSSLayerRule.class::isInstance, nIndex, CSSLayerRule.class::cast); + } + + /** + * Get a list of all top-level rules that are layer rules (implementing {@link CSSLayerRule}). + * + * @return A copy of all contained layer rules. Never null. + * @since 8.2.0 + */ + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllLayerRules () + { + return m_aRules.getAllMapped (CSSLayerRule.class::isInstance, CSSLayerRule.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. + * @since 8.2.0 + */ + 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 property rules. Always ≥ 0. + * @since 8.2.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 #getStyleRuleCount()}. + * @return The property rule at the given index, or null if an invalid index was specified. + * @since 8.2.0 + */ + @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. + * @since 8.2.0 + */ + @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 a style rule (implementing * {@link CSSStyleRule}). diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java b/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java index 13e3ee3f..09dd3120 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java @@ -35,7 +35,7 @@ import com.helger.css.property.ECSSProperty; /** - * Represents a single element in a CSS style rule. (eg. color:red; + * Represents a single element in a CSS style rule. (e.g. color:red; * or background:uri(a.gif) !important;)
* Instances of this class are mutable since 3.7.4. * @@ -99,7 +99,7 @@ public final String getProperty () @NonNull private static String _unifyProperty (@NonNull final String sProperty) { - // CSS variables are case sensitive (see issue 63) + // CSS variables are case-sensitive (see issue 63) if (sProperty.startsWith ("--")) return sProperty; return sProperty.toLowerCase (Locale.ROOT); @@ -107,7 +107,7 @@ private static String _unifyProperty (@NonNull final String sProperty) /** * Check if this declaration has the specified property. The comparison is - * case insensitive! + * case-insensitive! * * @param sProperty * The property to check. May not be null. @@ -123,7 +123,7 @@ public final boolean hasProperty (@NonNull final String sProperty) /** * Check if this declaration has the specified property. The comparison is - * case insensitive! + * case-insensitive! * * @param eProperty * The property to check. May not be null. diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSDeclarationContainer.java b/ph-css/src/main/java/com/helger/css/decl/CSSDeclarationContainer.java index cba24b6c..e3764622 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSDeclarationContainer.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSDeclarationContainer.java @@ -36,6 +36,13 @@ public class CSSDeclarationContainer extends CSSDeclarationList public CSSDeclarationContainer () {} + @NonNull + @Nonempty + public String getDeclarationsAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) + { + return super.getAsCSSString (aSettings, nIndentLevel); + } + @Override @NonNull @Nonempty @@ -55,16 +62,18 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn { // A single declaration aSB.append (bOptimizedOutput ? "{" : " { "); - aSB.append (super.getAsCSSString (aSettings, nIndentLevel)); + aSB.append (super.getAsCSSString (aSettings, nIndentLevel + 1)); aSB.append (bOptimizedOutput ? "}" : " }"); } else { // More than one declaration aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); - aSB.append (super.getAsCSSString (aSettings, nIndentLevel)); if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getIndent (nIndentLevel + 1)); + aSB.append (super.getAsCSSString (aSettings, nIndentLevel + 1)); + if (!bOptimizedOutput) + aSB.append(aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); } } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSExpressionMemberLineNames.java b/ph-css/src/main/java/com/helger/css/decl/CSSExpressionMemberLineNames.java index 879f7be1..8a3e3af9 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSExpressionMemberLineNames.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSExpressionMemberLineNames.java @@ -62,22 +62,22 @@ public CSSExpressionMemberLineNames addMember (@NonNull @Nonempty final String s } @NonNull - public CSSExpressionMemberLineNames addMember (@Nonnegative final int nIndex, @NonNull @Nonempty final String aMember) + public CSSExpressionMemberLineNames addMember (@Nonnegative final int nIndex, @NonNull @Nonempty final String sMember) { ValueEnforcer.isGE0 (nIndex, "Index"); - ValueEnforcer.notNull (aMember, "Member"); + ValueEnforcer.notNull (sMember, "Member"); if (nIndex >= getMemberCount ()) - m_aMembers.add (aMember); + m_aMembers.add (sMember); else - m_aMembers.add (nIndex, aMember); + m_aMembers.add (nIndex, sMember); return this; } @NonNull - public EChange removeMember (@NonNull final String aMember) + public EChange removeMember (@NonNull final String sMember) { - return m_aMembers.removeObject (aMember); + return m_aMembers.removeObject (sMember); } @NonNull diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java index 10b428b0..019cb72e 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java @@ -34,13 +34,15 @@ import com.helger.css.ICSSWriterSettings; /** - * Represents a single @font-face rule.
- * Example:
- * @font-face { - font-family: 'icons'; - src: url(path/to/font.woff) format('woff'); - unicode-range: U+E000-E005; -} + * Represents a single @font-face rule. + * + *

Example: + * + *

@font-face {
+  font-family: 'icons';
+  src: url(path/to/font.woff) format('woff');
+  unicode-range: U+E000-E005;
+}
* * @author Philip Helger */ @@ -166,11 +168,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (aSettings.isRemoveUnnecessaryCode () && !hasDeclarations ()) return ""; - final StringBuilder aSB = new StringBuilder (m_sDeclaration); - aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); - if (!aSettings.isOptimizedOutput ()) - aSB.append (aSettings.getNewLineString ()); - return aSB.toString (); + return m_sDeclaration + m_aDeclarations.getAsCSSString(aSettings, nIndentLevel); } @Nullable diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSImportRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSImportRule.java index d4b5cf5f..d03e757b 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSImportRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSImportRule.java @@ -234,7 +234,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn aSB.append (aMediaQuery.getAsCSSString (aSettings, nIndentLevel)); } } - return aSB.append (';').append (aSettings.getNewLineString ()).toString (); + return aSB.append (';').toString (); } @Nullable diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSKeyframesRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSKeyframesRule.java index a094735c..a8cd9c6d 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSKeyframesRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSKeyframesRule.java @@ -35,13 +35,14 @@ import com.helger.css.ICSSWriterSettings; /** - * Represents a single @keyframes rule.
- * Example:
- * @keyframes identifier { + * Represents a single @keyframes rule. + * + *

Example: + * + *

@keyframes identifier {
   0% { top: 0; left: 0; }
   30% { top: 50px; }
- }
- *
+}
* @author Philip Helger */ @NotThreadSafe @@ -161,14 +162,17 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (!aSettings.isWriteKeyframesRules ()) return ""; - if (aSettings.isRemoveUnnecessaryCode () && m_aBlocks.isEmpty ()) + int nBlockCount = m_aBlocks.size (); + boolean bFirst = true; + + if (aSettings.isRemoveUnnecessaryCode () && nBlockCount == 0) return ""; final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); final StringBuilder aSB = new StringBuilder (m_sDeclaration); aSB.append (' ').append (m_sAnimationName).append (bOptimizedOutput ? "{" : " {"); - if (!bOptimizedOutput) + if (!bOptimizedOutput && nBlockCount > 0) aSB.append (aSettings.getNewLineString ()); // Add all blocks @@ -177,18 +181,19 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn final String sBlockCSS = aBlock.getAsCSSString (aSettings, nIndentLevel + 1); if (StringHelper.isNotEmpty (sBlockCSS)) { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aSB.append (aSettings.getNewLineString ()); if (!bOptimizedOutput) aSB.append (aSettings.getIndent (nIndentLevel + 1)); aSB.append (sBlockCSS); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); } } - if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + if (!bOptimizedOutput && nBlockCount > 0) + aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); return aSB.toString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSLayerRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSLayerRule.java index 6edab84c..263eea34 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSLayerRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSLayerRule.java @@ -16,6 +16,7 @@ */ package com.helger.css.decl; +import com.helger.base.state.EChange; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -33,7 +34,7 @@ import com.helger.css.ICSSWriterSettings; @NotThreadSafe -public class CSSLayerRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware +public class CSSLayerRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware { private final ICommonsList m_aSelectors; private CSSSourceLocation m_aSourceLocation; @@ -50,6 +51,121 @@ public CSSLayerRule (@NonNull final Iterable aSelectors) m_aSelectors = new CommonsArrayList <> (aSelectors); } + /** + * Checks if at least one selector is present. + * @return true if at least one selector is present, false otherwise. + * @since 8.2.0 + */ + public boolean hasSelectors () + { + return m_aSelectors.isNotEmpty (); + } + + /** + * Gets the number of selectors. + * @return The number of selectors. Always ≥ 0. + * @since 8.2.0 + */ + @Nonnegative + public int getSelectorCount () + { + return m_aSelectors.size (); + } + + /** + * Adds a selector to the end of the selector list. + * @param sSelector The selector to be added. Must not be null. + * @return This rule for chaining. Never null. + * @since 8.2.0 + */ + @NonNull + public CSSLayerRule addSelector (@NonNull final String sSelector) + { + ValueEnforcer.notNull (sSelector, "Selector"); + + m_aSelectors.add (sSelector); + return this; + } + + /** + * Adds a selector at the specified index. If the index is greater than the current number of selectors, the selector + * is added at the end of the list. + * @param nIndex The index at which the selector should be added. Must be ≥ 0. + * @param sSelector The selector to be added. Must not be null. + * @return This rule for chaining. Never null. + * @since 8.2.0 + */ + @NonNull + public CSSLayerRule addSelector (@Nonnegative final int nIndex, @NonNull final String sSelector) + { + ValueEnforcer.isGE0 (nIndex, "Index"); + ValueEnforcer.notNull (sSelector, "Selector"); + + if (nIndex >= getSelectorCount ()) + m_aSelectors.add (sSelector); + else + m_aSelectors.add (nIndex, sSelector); + return this; + } + + /** + * Remove the specified selector, if present. + * + * @param sSelector The selector to be removed. Must not be null. + * @return {@link EChange#CHANGED} if the selector was removed, {@link EChange#UNCHANGED} if the selector was not found. + * Never null. + * @since 8.2.0 + */ + @NonNull + public EChange removeSelector (@NonNull final String sSelector) + { + return m_aSelectors.removeObject (sSelector); + } + + /** + * Removes the selector at the specified index. + * + * @param nSelectorIndex The index of the selector to be removed. Must be ≥ 0. + * @return {@link EChange#CHANGED} if the selector was removed, {@link EChange#UNCHANGED} if the index was ≥ the + * number of selectors. Never null. + * @since 8.2.0 + */ + @NonNull + public EChange removeSelector (@Nonnegative final int nSelectorIndex) + { + return m_aSelectors.removeAtIndex (nSelectorIndex); + } + + /** + * Removes all selectors. + * + * @return {@link EChange#CHANGED} if any selector was removed, + * {@link EChange#UNCHANGED} otherwise. Never null. + * @since 8.2.0 + */ + @NonNull + public EChange removeAllSelectors () + { + return m_aSelectors.removeAll (); + } + + /** + * Gets the selector at the specified index. + * + * @param nSelectorIndex The index of the selector to be retrieved. Must be ≥ 0. + * @return The selector at the specified index, or null if the index is ≥ the number of selectors. + * @since 8.2.0 + */ + @Nullable + public String getSelectorAtIndex (@Nonnegative final int nSelectorIndex) + { + return m_aSelectors.getAtIndex (nSelectorIndex); + } + + /** + * Gets a copy of all selectors. Modifications to the returned list do not affect this rule, and vice versa. + * @return A list of all selectors. Never null. + */ @NonNull @ReturnsMutableCopy public ICommonsList getAllSelectors () @@ -60,6 +176,10 @@ public ICommonsList getAllSelectors () @NonNull public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) { + // Always ignore layer rules? + if (!aSettings.isWriteLayerRules ()) + return ""; + final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); final StringBuilder aSB = new StringBuilder ("@layer "); @@ -103,13 +223,10 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn } } if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append(aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); } - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); - return aSB.toString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSMediaRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSMediaRule.java index 23997ab5..eac3d5eb 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSMediaRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSMediaRule.java @@ -36,18 +36,20 @@ /** * Represents a single @media rule: a list of style rules only valid for certain - * media.
- * Example:
- * @media print { + * media. + * + *

Example: + * + *

@media print {
   div#footer {
     display: none;
   }
-}
+}
* * @author Philip Helger */ @NotThreadSafe -public class CSSMediaRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware +public class CSSMediaRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware { private final ICommonsList m_aMediaQueries = new CommonsArrayList <> (); private CSSSourceLocation m_aSourceLocation; @@ -201,7 +203,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (nRuleCount == 0) { - aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); + aSB.append (bOptimizedOutput ? "{}" : " {}"); } else { @@ -217,7 +219,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn bFirst = false; else if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getNewLineString ()); if (!bOptimizedOutput) aSB.append (aSettings.getIndent (nIndentLevel + 1)); @@ -225,10 +227,8 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn } } if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append(aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); } return aSB.toString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java index ac4ac99d..4cb44af5 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java @@ -120,7 +120,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn aSB.append (CSSURLHelper.getAsCSSURL (m_sURL, false)); else aSB.append ("\"\""); - return aSB.append (';').append (aSettings.getNewLineString ()).toString (); + return aSB.append (';').toString (); } @Nullable diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java b/ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java new file mode 100644 index 00000000..adfb6cc8 --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java @@ -0,0 +1,215 @@ +/* + * 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 com.helger.annotation.Nonnegative; +import com.helger.annotation.concurrent.NotThreadSafe; +import com.helger.annotation.style.ReturnsMutableCopy; +import com.helger.base.hashcode.HashCodeGenerator; +import com.helger.base.state.EChange; +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; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Represents nested style declarations. When nesting rules, all CSS style declarations after nested rules are wrapped + * within a nested declarations block, in accordance with the CSS Nesting Module Level 1 specification. A nested + * declarations instance consists of a number of declarations (the styles to be applied to the selected elements). + * + *

Example: + * + *

div {
+  color: red;
+  span {
+    color: green;
+  }
+  color: blue;
+}
+ * + * In the above example, color: blue; will be placed inside a nested declarations instances, as a child + * of a {@link CSSStyleRule}. The resulting object structure will look like this: + * + *
    + *
  • A {@link CSSStyleRule} representing the entire div { ... } block + *
      + *
    • The {@link CSSStyleRule#getAllDeclarations()} with color: red;
    • + *
    • The {@link CSSStyleRule#getAllRules()} with
    • + *
        + *
      • A nested {@link CSSStyleRule} represent span { color: green; } + *
      • A nested {@link CSSNestedDeclarations} representing color: blue;
      • + *
      + *
    + *
+ * @author Philip Helger + * @since 8.2.0 + */ +@NotThreadSafe +public class CSSNestedDeclarations implements ICSSNestedRule, IHasCSSDeclarations , ICSSSourceLocationAware +{ + private final CSSDeclarationContainer m_aDeclarations = new CSSDeclarationContainer (); + private CSSSourceLocation m_aSourceLocation; + + /** + * Creates a new, empty instance with no declarations. + */ + public CSSNestedDeclarations() + {} + + @Override + @NonNull + public CSSNestedDeclarations addDeclaration (@NonNull final CSSDeclaration aDeclaration) + { + m_aDeclarations.addDeclaration (aDeclaration); + return this; + } + + @Override + @NonNull + public CSSNestedDeclarations addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) + { + m_aDeclarations.addDeclaration (nIndex, aNewDeclaration); + return this; + } + + @Override + @NonNull + public EChange removeDeclaration (@NonNull final CSSDeclaration aDeclaration) + { + return m_aDeclarations.removeDeclaration (aDeclaration); + } + + @Override + @NonNull + public EChange removeDeclaration (@Nonnegative final int nDeclarationIndex) + { + return m_aDeclarations.removeDeclaration (nDeclarationIndex); + } + + @Override + @NonNull + public EChange removeAllDeclarations () + { + return m_aDeclarations.removeAllDeclarations (); + } + + @Override + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllDeclarations () + { + return m_aDeclarations.getAllDeclarations (); + } + + @Override + @Nullable + public CSSDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) + { + return m_aDeclarations.getDeclarationAtIndex (nIndex); + } + + @Override + @NonNull + public CSSNestedDeclarations setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) + { + m_aDeclarations.setDeclarationAtIndex (nIndex, aNewDeclaration); + return this; + } + + @Override + public boolean hasDeclarations () + { + return m_aDeclarations.hasDeclarations (); + } + + @Override + @Nonnegative + public int getDeclarationCount () + { + return m_aDeclarations.getDeclarationCount (); + } + + @Override + @Nullable + public CSSDeclaration getDeclarationOfPropertyName (@Nullable final String sPropertyName) + { + return m_aDeclarations.getDeclarationOfPropertyName (sPropertyName); + } + + @Override + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllDeclarationsOfPropertyName (@Nullable final String sPropertyName) + { + return m_aDeclarations.getAllDeclarationsOfPropertyName (sPropertyName); + } + + @Override + @NonNull + public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) + { + // Always ignore nested declarations? + if (!aSettings.isWriteNestedDeclarations ()) + return ""; + + if (aSettings.isRemoveUnnecessaryCode () && !hasDeclarations ()) + return ""; + + return m_aDeclarations.getDeclarationsAsCSSString(aSettings, nIndentLevel); + } + + @Override + @Nullable + public final CSSSourceLocation getSourceLocation () + { + return m_aSourceLocation; + } + + @Override + 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 CSSNestedDeclarations rhs = (CSSNestedDeclarations) o; + return m_aDeclarations.equals (rhs.m_aDeclarations); + } + + @Override + public int hashCode () + { + return new HashCodeGenerator (this).append (m_aDeclarations).getHashCode (); + } + + @Override + public String toString () + { + return new ToStringGenerator (this).append ("declarations", m_aDeclarations) + .appendIfNotNull ("SourceLocation", m_aSourceLocation) + .getToString (); + } +} diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPageRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPageRule.java index e9dd1297..02b678f3 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPageRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPageRule.java @@ -16,6 +16,7 @@ */ package com.helger.css.decl; +import com.helger.css.CCSS; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -34,12 +35,13 @@ import com.helger.css.ICSSWriterSettings; /** - * Represents a single @page rule.
- * Example:
- * @page { + * Represents a single @page rule. + *

Example: + * + *

@page {
   size: auto;
   margin: 10%;
-}
+}
* * @author Philip Helger */ @@ -170,23 +172,23 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn { // A single declaration aSB.append (bOptimizedOutput ? "{" : " { "); - aSB.append (m_aMembers.getAsCSSString (aSettings, nIndentLevel)); + aSB.append (getPageRuleMemberAsCSS(aSettings, nIndentLevel + 1)); aSB.append (bOptimizedOutput ? "}" : " }"); } else { // More than one declaration aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); - aSB.append (m_aMembers.getAsCSSString (aSettings, nIndentLevel)); + if (!bOptimizedOutput) { + aSB.append (aSettings.getIndent(nIndentLevel + 1)); + } + aSB.append (getPageRuleMemberAsCSS(aSettings, nIndentLevel + 1)); if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); } } - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); - return aSB.toString (); } @@ -225,4 +227,43 @@ public String toString () .appendIfNotNull ("SourceLocation", m_aSourceLocation) .getToString (); } + + private String getPageRuleMemberAsCSS(@NonNull ICSSWriterSettings aSettings, int nIndentLevel) { + final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); + + final int nDeclCount = m_aMembers.size (); + if (nDeclCount == 0) + return ""; + if (nDeclCount == 1) + { + // A single element + final StringBuilder aSB = new StringBuilder (); + aSB.append (m_aMembers.get (0).getAsCSSString (aSettings, nIndentLevel)); + // No ';' at the last entry + if (m_aMembers.get (0) instanceof CSSDeclaration) + if (!bOptimizedOutput) + aSB.append (CCSS.DEFINITION_END); + return aSB.toString (); + } + + // More than one element + final StringBuilder aSB = new StringBuilder (); + int nIndex = 0; + for (final ICSSPageRuleMember aElement : m_aMembers) + { + // Indentation + if (!bOptimizedOutput && nIndex != 0) + aSB.append (aSettings.getIndent (nIndentLevel)); + // Emit the main element plus the semicolon + aSB.append (aElement.getAsCSSString (aSettings, nIndentLevel )); + // No ';' at the last decl + if (aElement instanceof CSSDeclaration) + if (!bOptimizedOutput || nIndex < nDeclCount - 1) + aSB.append (CCSS.DEFINITION_END); + if (!bOptimizedOutput && nIndex != m_aMembers.size() -1) + aSB.append (aSettings.getNewLineString ()); + ++nIndex; + } + return aSB.toString (); + } } 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..6457234e --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -0,0 +1,225 @@ +/* + * 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 com.helger.annotation.Nonempty; +import com.helger.annotation.Nonnegative; +import com.helger.annotation.concurrent.NotThreadSafe; +import com.helger.base.enforce.ValueEnforcer; +import com.helger.base.hashcode.HashCodeGenerator; +import com.helger.base.string.StringHelper; +import com.helger.base.tostring.ToStringGenerator; +import com.helger.css.CSSSourceLocation; +import com.helger.css.ICSSSourceLocationAware; +import com.helger.css.ICSSWriterSettings; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Represents a single @viewport rule. + * + *

Example: + * + *

@property --rotation {
+  syntax: '<angle>';
+  inherits: false;
+  initial-value: 45deg;
+}
+ * + * @author Philip Helger + * @since 8.2.0 + */ +@NotThreadSafe +public class CSSPropertyRule implements ICSSTopLevelRule, ICSSSourceLocationAware +{ + private String m_sName; + private String m_sInitialValue; + private Boolean m_bInherits; + private String m_sSyntax; + private CSSSourceLocation m_aSourceLocation; + + /** + * Checks if the passed property name is a valid custom property name. A valid custom property name starts with + * --. + * @param sPropertyName The property name to check. May not be null or empty. + * @return true if the passed property name is valid, false otherwise. + */ + public static boolean isValidPropertyName (@NonNull @Nonempty final String sPropertyName) + { + return StringHelper.startsWith (sPropertyName, "--"); + } + + public CSSPropertyRule(@NonNull @Nonempty final String sPropertyName) + { + ValueEnforcer.isTrue (isValidPropertyName (sPropertyName), "Property name is invalid"); + m_sName = sPropertyName; + } + + /** + * @return The property name. Neither null nor empty. Always starts with --. + */ + @NonNull + @Nonempty + public String getName () + { + return m_sName; + } + + public void setName(String name) { + ValueEnforcer.isTrue (isValidPropertyName (name), "Property name is invalid"); + this.m_sName = name; + } + + + @NonNull + public String getInitialValue () + { + return m_sInitialValue != null ? m_sInitialValue : ""; + } + + public void setInitialValue(String initialValue) { + this.m_sInitialValue = initialValue; + } + + public boolean isInherits () + { + return m_bInherits != null ? m_bInherits : false; + } + + public void setInherits(Boolean inherits) { + this.m_bInherits = inherits; + } + + @NonNull + public String getSyntax () + { + return m_sSyntax != null ? m_sSyntax : ""; + } + + public void setSyntax(String syntax) { + this.m_sSyntax = syntax; + } + + @Override + @NonNull + @Nonempty + public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) + { + // Always ignore property rules? + if (!aSettings.isWritePropertyRules ()) + return ""; + + final boolean bOptimizeOutput = aSettings.isOptimizedOutput (); + final List> aDeclarations = _buildDeclarations(); + final int nCount = aDeclarations.size (); + + final StringBuilder aSB = new StringBuilder ("@property "); + aSB.append(m_sName); + + aSB.append (bOptimizeOutput ? "{" : " {"); + if (!bOptimizeOutput && nCount == 1) + aSB.append (" "); + + int nIndex = 0; + for (final Map.Entry aDeclaration : aDeclarations) + { + if (!bOptimizeOutput && nCount > 1) + aSB.append (aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel + 1)); + aSB.append (aDeclaration.getKey ()); + aSB.append (":"); + aSB.append (aDeclaration.getValue ()); + if (!bOptimizeOutput || nIndex != aDeclarations.size() - 1) + aSB.append (";"); + ++nIndex; + } + + if (!bOptimizeOutput && nCount > 1) + aSB.append (aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel)); + if (!bOptimizeOutput && nCount == 1) + aSB.append (" "); + aSB.append("}"); + + return aSB.toString (); + } + + @Override + @Nullable + public final CSSSourceLocation getSourceLocation () + { + return m_aSourceLocation; + } + + @Override + 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_sName.equals (rhs.m_sName) && m_sInitialValue.equals (rhs.m_sInitialValue) && m_bInherits == rhs.m_bInherits && m_sSyntax.equals (rhs.m_sSyntax); + } + + @Override + public int hashCode () + { + return new HashCodeGenerator (this).append (m_sName).append (m_bInherits).append(m_sSyntax).append(m_sInitialValue).getHashCode (); + } + + @Override + public String toString () + { + return new ToStringGenerator (this).append ("propertyName", m_sName) + .append ("syntax", m_sSyntax) + .append ("inherits", m_bInherits) + .append ("initialValue", m_sInitialValue) + .appendIfNotNull ("SourceLocation", m_aSourceLocation) + .getToString (); + } + + private List> _buildDeclarations() + { + final List> ret = new ArrayList<> (); + + if (StringHelper.isNotEmpty(m_sSyntax)) + { + ret.add (Map.entry ("syntax", m_sSyntax)); + } + + if (m_bInherits != null) + { + ret.add (Map.entry ("inherits", Boolean.toString(m_bInherits))); + } + + if (StringHelper.isNotEmpty(m_sInitialValue)) + { + ret.add (Map.entry ("initial-value", m_sInitialValue)); + } + + return ret; + } +} diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java index 0fdcc4f4..6418752b 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java @@ -55,11 +55,11 @@ public String getValue () } /** - * @return true if it is no hash, no class and no pseudo selector + * @return true if it is no hash, no class, no pseudo, and no nesting selector */ public boolean isElementName () { - return !isHash () && !isClass () && !isPseudo (); + return !isHash () && !isClass () && !isPseudo () && !isNesting(); } /** @@ -86,6 +86,16 @@ public boolean isPseudo () return m_sValue.charAt (0) == ':'; } + /** + * Checks if this selector represents the nesting selector &. + * @return true if it is a nesting selector + * @since 8.2.0 + */ + public boolean isNesting () + { + return m_sValue.charAt (0) == '&'; + } + @NonNull @Nonempty public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSStyleRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSStyleRule.java index 988bdbaf..e7406bdb 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSStyleRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSStyleRule.java @@ -16,6 +16,7 @@ */ package com.helger.css.decl; +import com.helger.css.CCSS; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -34,19 +35,29 @@ /** * Represents a single CSS style rule. A style rule consists of a number of - * selectors (determine the element to which the style rule applies) and a - * number of declarations (the rules to be applied to the selected elements). - *
- * Example:
- * div { color: red; } + * {@link CSSSelector selectors} (determines the elements to which + * the style rule applies), a number of {@link CSSDeclarationContainer declarations} + * (the styles to be applied to the selected elements), and a number of + * {@link ICSSNestedRule nested rules} (the rules nested within the style rule, + * e.g. media rules, supports rules, or nested declarations). + * + *

Example: + * + *

div {
+  color: red;
+  &:hover {
+    color: blue;
+  }
+}
* * @author Philip Helger */ @NotThreadSafe -public class CSSStyleRule implements ICSSTopLevelRule, IHasCSSDeclarations , ICSSSourceLocationAware +public class CSSStyleRule implements ICSSTopLevelRule, ICSSNestedRule, IHasCSSDeclarations , IHasCSSNestedRules, ICSSSourceLocationAware { private final ICommonsList m_aSelectors = new CommonsArrayList <> (); private final CSSDeclarationContainer m_aDeclarations = new CSSDeclarationContainer (); + private final ICommonsList m_aRules = new CommonsArrayList <> (); private CSSSourceLocation m_aSourceLocation; public CSSStyleRule () @@ -139,6 +150,80 @@ public ICommonsList getAllSelectors () return m_aSelectors.getClone (); } + @Override + public boolean hasRules() + { + return m_aRules.isNotEmpty (); + } + + @Override + @Nonnegative + public int getRuleCount() + { + return m_aRules.size (); + } + + @Override + @NonNull + public CSSStyleRule addRule(@NonNull final ICSSNestedRule aRule) + { + ValueEnforcer.notNull (aRule, "Rule"); + + m_aRules.add (aRule); + return this; + } + + @Override + @NonNull + public CSSStyleRule addRule(@Nonnegative final int nIndex, @NonNull final ICSSNestedRule aRule) + { + ValueEnforcer.isGE0 (nIndex, "Index"); + ValueEnforcer.notNull (aRule, "Rule"); + + if (nIndex >= getRuleCount ()) + m_aRules.add (aRule); + else + m_aRules.add (nIndex, aRule); + return this; + } + + @Override + @NonNull + public EChange removeRule(@NonNull final ICSSNestedRule aRule) + { + return m_aRules.removeObject (aRule); + } + + @Override + @NonNull + public EChange removeRule(@Nonnegative final int nRuleIndex) + { + return m_aRules.removeAtIndex (nRuleIndex); + } + + @Override + @NonNull + public EChange removeAllRules() + { + return m_aRules.removeAll (); + } + + @Override + @Nullable + public ICSSNestedRule getRuleAtIndex(@Nonnegative final int nRuleIndex) + { + return m_aRules.getAtIndex (nRuleIndex); + } + + @Override + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllRules() + { + return m_aRules.getClone (); + } + + @Override @NonNull public CSSStyleRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) { @@ -146,6 +231,7 @@ public CSSStyleRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) return this; } + @Override @NonNull public CSSStyleRule addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) { @@ -153,24 +239,28 @@ public CSSStyleRule addDeclaration (@Nonnegative final int nIndex, @NonNull fina return this; } + @Override @NonNull public EChange removeDeclaration (@NonNull final CSSDeclaration aDeclaration) { return m_aDeclarations.removeDeclaration (aDeclaration); } + @Override @NonNull public EChange removeDeclaration (@Nonnegative final int nDeclarationIndex) { return m_aDeclarations.removeDeclaration (nDeclarationIndex); } + @Override @NonNull public EChange removeAllDeclarations () { return m_aDeclarations.removeAllDeclarations (); } + @Override @NonNull @ReturnsMutableCopy public ICommonsList getAllDeclarations () @@ -178,12 +268,14 @@ public ICommonsList getAllDeclarations () return m_aDeclarations.getAllDeclarations (); } + @Override @Nullable public CSSDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) { return m_aDeclarations.getDeclarationAtIndex (nIndex); } + @Override @NonNull public CSSStyleRule setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) { @@ -191,23 +283,27 @@ public CSSStyleRule setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNu return this; } + @Override public boolean hasDeclarations () { return m_aDeclarations.hasDeclarations (); } + @Override @Nonnegative public int getDeclarationCount () { return m_aDeclarations.getDeclarationCount (); } + @Override @Nullable public CSSDeclaration getDeclarationOfPropertyName (@Nullable final String sPropertyName) { return m_aDeclarations.getDeclarationOfPropertyName (sPropertyName); } + @Override @NonNull @ReturnsMutableCopy public ICommonsList getAllDeclarationsOfPropertyName (@Nullable final String sPropertyName) @@ -215,6 +311,16 @@ public ICommonsList getAllDeclarationsOfPropertyName (@Nullable return m_aDeclarations.getAllDeclarationsOfPropertyName (sPropertyName); } + /** + * Get the selectors as a serialized CSS string for writing to an output. + * + * @param aSettings + * The settings to be used to format the output. May not be + * null. + * @param nIndentLevel + * The current indentation level + * @return The content of the selectors as CSS string. Never null. + */ @NonNull public String getSelectorsAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) { @@ -236,32 +342,111 @@ public String getSelectorsAsCSSString (@NonNull final ICSSWriterSettings aSettin return aSB.toString (); } + @Override @NonNull public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) { - if (aSettings.isRemoveUnnecessaryCode () && !hasDeclarations ()) + if (aSettings.isRemoveUnnecessaryCode () && !hasDeclarations () && !hasRules()) return ""; final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); + final int nDeclCount = m_aDeclarations.getDeclarationCount (); + final int nRuleCount = m_aRules.size (); + final int nElementCount = nDeclCount + nRuleCount; final StringBuilder aSB = new StringBuilder (); // Append the selectors aSB.append (getSelectorsAsCSSString (aSettings, nIndentLevel)); + // Append the opening brace + if (nElementCount == 0) + aSB.append (bOptimizedOutput ? "{" : " {"); + else + if (nElementCount == 1) + aSB.append (bOptimizedOutput ? "{" : " { "); + else + aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); + // Append the declarations - aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); + if (nDeclCount == 1 && nRuleCount == 0) + { + aSB.append (m_aDeclarations.get(0).getAsCSSString (aSettings, nIndentLevel)); + // No ';' at the last entry + if (!bOptimizedOutput) + aSB.append (CCSS.DEFINITION_END); + } + else + if (nDeclCount >= 1) { + int nIndex = 0; + for (final CSSDeclaration aDeclaration : m_aDeclarations) + { + // Indentation + if (!bOptimizedOutput) + aSB.append (aSettings.getIndent (nIndentLevel + 1)); + // Emit the main element plus the semicolon + aSB.append (aDeclaration.getAsCSSString (aSettings, nIndentLevel + 1)); + // No ';' at the last decl + if (!bOptimizedOutput || nIndex < nDeclCount - 1 || nRuleCount > 0) + aSB.append (CCSS.DEFINITION_END); + // No line break at the last decl + if (!bOptimizedOutput && nIndex != nDeclCount - 1) + aSB.append (aSettings.getNewLineString ()); + ++nIndex; + } + } + + // Empty line between declarations and nested rules + if (!bOptimizedOutput && (nDeclCount > 0 && nRuleCount > 0)) + aSB.append (aSettings.getNewLineString ()).append (aSettings.getNewLineString ()); + + // Append the rules + if (nRuleCount > 0) + { + boolean bFirst = true; + int nRuleIndex = 0; + for (final ICSSNestedRule aRule : m_aRules) + { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aSB.append (aSettings.getNewLineString ()).append (aSettings.getNewLineString ()); + + if (!bOptimizedOutput) + aSB.append (aSettings.getIndent (nIndentLevel + 1)); + aSB.append(aRule.getAsCSSString(aSettings, nIndentLevel + 1)); + // When outputting optimized, no semicolon is added after the last declaration + // But when there are more rules, we need a semicolon as a separator + if (bOptimizedOutput && aRule instanceof CSSNestedDeclarations && nRuleIndex != nRuleCount - 1) { + aSB.append (CCSS.DEFINITION_END); + } + + ++nRuleIndex; + } + } + + if (!bOptimizedOutput && nElementCount > 0) + // Add space if there is exactly one declaration and no rules. Otherwise, add a line break + if (nElementCount == 1 && nRuleCount == 0) + aSB.append(" "); + else + aSB.append(aSettings.getNewLineString()).append(aSettings.getIndent(nIndentLevel)); + + // Append the closing brace + aSB.append ("}"); + return aSB.toString (); } + @Override @Nullable public final CSSSourceLocation getSourceLocation () { return m_aSourceLocation; } + @Override public final void setSourceLocation (@Nullable final CSSSourceLocation aSourceLocation) { m_aSourceLocation = aSourceLocation; @@ -275,13 +460,14 @@ public boolean equals (final Object o) if (o == null || !getClass ().equals (o.getClass ())) return false; final CSSStyleRule rhs = (CSSStyleRule) o; - return m_aSelectors.equals (rhs.m_aSelectors) && m_aDeclarations.equals (rhs.m_aDeclarations); + return m_aSelectors.equals (rhs.m_aSelectors) && m_aDeclarations.equals (rhs.m_aDeclarations) + && m_aRules.equals (rhs.m_aRules); } @Override public int hashCode () { - return new HashCodeGenerator (this).append (m_aSelectors).append (m_aDeclarations).getHashCode (); + return new HashCodeGenerator (this).append (m_aSelectors).append (m_aDeclarations).append(m_aRules).getHashCode (); } @Override @@ -289,6 +475,7 @@ public String toString () { return new ToStringGenerator (this).append ("selectors", m_aSelectors) .append ("declarations", m_aDeclarations) + .append ("rules", m_aRules) .appendIfNotNull ("SourceLocation", m_aSourceLocation) .getToString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java index 85ca62c2..5c21b0a4 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java @@ -36,16 +36,18 @@ /** * Represents a single @supports 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) { + * declaration is available. See {@link com.helger.css.ECSSSpecification#CSS3_CONDITIONAL} + * + *

Example: + * + *

@supports (transition-property: color) {
   div { color:red; }
-}
+}
* * @author Philip Helger */ @NotThreadSafe -public class CSSSupportsRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware +public class CSSSupportsRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware { private final ICommonsList m_aConditionMembers = new CommonsArrayList <> (); private CSSSourceLocation m_aSourceLocation; @@ -152,7 +154,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (nRuleCount == 0) { - aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); + aSB.append (bOptimizedOutput ? "{}" : " {}"); } else { @@ -176,10 +178,8 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn } } if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); } return aSB.toString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSUnknownRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSUnknownRule.java index 5f163c57..f66f8af3 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSUnknownRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSUnknownRule.java @@ -36,7 +36,7 @@ * @author Philip Helger */ @NotThreadSafe -public class CSSUnknownRule implements ICSSTopLevelRule, ICSSSourceLocationAware +public class CSSUnknownRule implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware { private final String m_sDeclaration; private String m_sParameterList; @@ -117,20 +117,18 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (StringHelper.isEmpty (m_sBody)) { - aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); + aSB.append (bOptimizedOutput ? "{}" : " {}"); } else { // At least one rule present aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getIndent (nIndentLevel + 1)); aSB.append (m_sBody); if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); } return aSB.toString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSViewportRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSViewportRule.java index a820ad07..e3d1757a 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSViewportRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSViewportRule.java @@ -34,9 +34,13 @@ import com.helger.css.ICSSWriterSettings; /** - * Represents a single @viewport rule.
- * Example:
- * @viewport { width: device-width; } + * Represents a single @viewport rule. + * + *

Example: + * + *

@viewport {
+  width: device-width;
+}
* * @author Philip Helger */ @@ -157,11 +161,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (aSettings.isRemoveUnnecessaryCode () && !hasDeclarations ()) return ""; - final StringBuilder aSB = new StringBuilder (m_sDeclaration); - aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); - if (!aSettings.isOptimizedOutput ()) - aSB.append (aSettings.getNewLineString ()); - return aSB.toString (); + return m_sDeclaration + m_aDeclarations.getAsCSSString(aSettings, nIndentLevel); } @Nullable diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java b/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java index 44ffaf8e..cc026a75 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java @@ -74,14 +74,14 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn for (final DATATYPE aElement : this) { // Indentation - if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel + 1)); + if (!bOptimizedOutput && nIndex != 0) + aSB.append (aSettings.getIndent (nIndentLevel)); // Emit the main element plus the semicolon - aSB.append (aElement.getAsCSSString (aSettings, nIndentLevel + 1)); + aSB.append (aElement.getAsCSSString (aSettings, nIndentLevel )); // No ';' at the last decl if (!bOptimizedOutput || nIndex < nDeclCount - 1) aSB.append (CCSS.DEFINITION_END); - if (!bOptimizedOutput) + if (!bOptimizedOutput && nIndex != size() - 1) aSB.append (aSettings.getNewLineString ()); ++nIndex; } diff --git a/ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java b/ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java new file mode 100644 index 00000000..4beb8f3d --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java @@ -0,0 +1,49 @@ +/* + * 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 com.helger.annotation.style.MustImplementEqualsAndHashcode; +import com.helger.css.ICSSWriteable; + +/** + * Marker interface for all nested CSS elements that can occur in any order + * + *
    + *
  • layer rules - {@link CSSLayerRule} + *
  • media rules - {@link CSSMediaRule} + *
  • nested declarations - {@link CSSNestedDeclarations} + *
  • style rules - {@link CSSStyleRule} + *
  • supports rules - {@link CSSSupportsRule} + *
  • unknown rules - {@link CSSUnknownRule} + *
+ * + * To easily iterate over all rules contained in a {@link CascadingStyleSheet} + * you can use the + * {@link com.helger.css.decl.visit.CSSVisitor#visitCSS(CascadingStyleSheet, com.helger.css.decl.visit.ICSSVisitor) CSSVisitor#visitCSS(sheet, visitor)} + * method. An empty stub implementation of + * {@link com.helger.css.decl.visit.ICSSVisitor ICSSVisitor} is the class + * {@link com.helger.css.decl.visit.DefaultCSSVisitor DefaultCSSVisitor} which is a good basis for + * your own implementations. + * + * @author Philip Helger + * @since 8.2.0 + */ +@MustImplementEqualsAndHashcode +public interface ICSSNestedRule extends ICSSWriteable +{ + /* empty */ +} diff --git a/ph-css/src/main/java/com/helger/css/decl/ICSSTopLevelRule.java b/ph-css/src/main/java/com/helger/css/decl/ICSSTopLevelRule.java index 933cc1cc..87756ba6 100644 --- a/ph-css/src/main/java/com/helger/css/decl/ICSSTopLevelRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/ICSSTopLevelRule.java @@ -20,28 +20,28 @@ import com.helger.css.ICSSWriteable; /** - *

* Marker interface for all top level CSS elements that can occur in any order - *

+ * *
    - *
  • font face rules - {@link CSSFontFaceRule}
  • - *
  • keyframes rules - {@link CSSKeyframesRule}
  • - *
  • media rules - {@link CSSMediaRule}
  • - *
  • page rules - {@link CSSPageRule}
  • - *
  • style rules - {@link CSSStyleRule}
  • - *
  • supports rules - {@link CSSSupportsRule}
  • - *
  • unknown rules - {@link CSSUnknownRule}
  • - *
  • viewport rules - {@link CSSViewportRule}
  • + *
  • font face rules - {@link CSSFontFaceRule} + *
  • keyframes rules - {@link CSSKeyframesRule} + *
  • layer rules - {@link CSSLayerRule} + *
  • media rules - {@link CSSMediaRule} + *
  • page rules - {@link CSSPageRule} + *
  • property rules - {@link CSSPropertyRule} + *
  • style rules - {@link CSSStyleRule} + *
  • supports rules - {@link CSSSupportsRule} + *
  • unknown rules - {@link CSSUnknownRule} + *
  • viewport rules - {@link CSSViewportRule} *
- *

+ * * To easily iterate over all rules contained in a {@link CascadingStyleSheet} * you can use the - * {@link com.helger.css.decl.visit.CSSVisitor#visitCSS(CascadingStyleSheet, com.helger.css.decl.visit.ICSSVisitor)} + * {@link com.helger.css.decl.visit.CSSVisitor#visitCSS(CascadingStyleSheet, com.helger.css.decl.visit.ICSSVisitor) CSSVisitor#visitCSS(sheet, visitor)} * method. An empty stub implementation of - * {@link com.helger.css.decl.visit.ICSSVisitor} is the class - * {@link com.helger.css.decl.visit.DefaultCSSVisitor} which is a good basis for + * {@link com.helger.css.decl.visit.ICSSVisitor ICSSVisitor} is the class + * {@link com.helger.css.decl.visit.DefaultCSSVisitor DefaultCSSVisitor} which is a good basis for * your own implementations. - *

* * @author Philip Helger */ diff --git a/ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java b/ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java new file mode 100644 index 00000000..cd93f975 --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java @@ -0,0 +1,90 @@ +package com.helger.css.decl; + +import com.helger.annotation.Nonnegative; +import com.helger.annotation.style.ReturnsMutableCopy; +import com.helger.base.state.EChange; +import com.helger.collection.commons.ICommonsList; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Sanity interface for all objects having nested CSS rules. + * + * @param + * Implementation type + * @author Philip Helger + * @since 8.2.0 + */ +public interface IHasCSSNestedRules { + /** + * Checks if this element has any nested rules. + * @return true if at least one nested rule is present, false otherwise. Never + * null. + */ + boolean hasRules(); + + /** + * Gets the number of nested rules contained in this element. + * @return The number of nested rules. Never negative. + */ + @Nonnegative + int getRuleCount(); + + /** + * Adds a rule to the list of nested rules. The rule is added at the end of the list. + * @param aRule The rule to be added. Must not be null. + * @return This element for chaining. Never null. + */ + @NonNull IMPLTYPE addRule(@NonNull ICSSNestedRule aRule); + + /** + * Adds a rule to the list of nested rules at the specified index. + * @param nIndex The index at which the rule should be added. If equal to or greater than the current number of rules, + * the rule will be added at the end of the list. Must not be negative. + * @param aRule The rule to be added. Must not be null. + * @return This element for chaining. Never null. + */ + @NonNull IMPLTYPE addRule(@Nonnegative int nIndex, @NonNull ICSSNestedRule aRule); + + /** + * Removes a rule from the list of nested rules, if present. + * @param aRule The rule to be removed. Must not be null. + * @return {@link EChange#CHANGED CHANGED} if the rule was removed, {@link EChange#UNCHANGED UNCHANGED} otherwise. + * Never null. + */ + @NonNull EChange removeRule(@NonNull ICSSNestedRule aRule); + + /** + * Removes a rule from the list of nested rules at the specified index. + * @param nRuleIndex The index of the rule to be removed. Values equal to or greater than the current number of rules + * will be ignored. Must not be negative. + * @return {@link EChange#CHANGED CHANGED} if the rule was removed, {@link EChange#UNCHANGED UNCHANGED} otherwise. + * Never null. + */ + @NonNull EChange removeRule(@Nonnegative int nRuleIndex); + + /** + * Remove all nested rules. + * + * @return {@link EChange#CHANGED CHANGED} if any rule was removed, {@link EChange#UNCHANGED UNCHANGED} otherwise. + * Never null. + */ + @NonNull EChange removeAllRules(); + + /** + * Gets a nested rule at the specified index. + * @param nRuleIndex The index of the rule to be retrieved. If equal to or greater than the current number of rules, + * null will be returned. Must not be negative. + * @return This rule for chaining. null if not found. + */ + @Nullable ICSSNestedRule getRuleAtIndex(@Nonnegative int nRuleIndex); + + /** + * Gets all nested rules contained in this element. Modifications to the returned list will not affect this style + * rule, and vice versa. + * @return A list of nested rules. Never null. + */ + @NonNull + @ReturnsMutableCopy + ICommonsList getAllRules(); +} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java index 42ad533c..4a0250af 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 @@ -16,6 +16,10 @@ */ package com.helger.css.decl.visit; +import com.helger.css.decl.CSSNestedDeclarations; +import com.helger.css.decl.CSSPropertyRule; +import com.helger.css.decl.ICSSNestedRule; +import com.helger.css.decl.IHasCSSNestedRules; import org.jspecify.annotations.NonNull; import com.helger.annotation.concurrent.Immutable; @@ -50,117 +54,122 @@ * @author Philip Helger */ @Immutable -public final class CSSVisitor -{ +public final class CSSVisitor { @PresentForCodeCoverage - private static final CSSVisitor INSTANCE = new CSSVisitor (); + private static final CSSVisitor INSTANCE = new CSSVisitor(); - private CSSVisitor () - {} + private CSSVisitor() { + } /** * Visit all elements of a single import rule. * - * @param aImportRule - * The import rule to visit. May not be null. - * @param aVisitor - * The visitor to use. May not be null. + * @param aImportRule The import rule to visit. May not be null. + * @param aVisitor The visitor to use. May not be null. */ - public static void visitImportRule (@NonNull final CSSImportRule aImportRule, @NonNull final ICSSVisitor aVisitor) - { - aVisitor.onImport (aImportRule); + public static void visitImportRule(@NonNull final CSSImportRule aImportRule, @NonNull final ICSSVisitor aVisitor) { + aVisitor.onImport(aImportRule); } /** * Visit all elements of a single namespace rule. * - * @param aNamespaceRule - * The namespace rule to visit. May not be null. - * @param aVisitor - * The visitor to use. May not be null. + * @param aNamespaceRule The namespace rule to visit. May not be null. + * @param aVisitor The visitor to use. May not be null. */ - public static void visitNamespaceRule (@NonNull final CSSNamespaceRule aNamespaceRule, @NonNull final ICSSVisitor aVisitor) - { - aVisitor.onNamespace (aNamespaceRule); + public static void visitNamespaceRule(@NonNull final CSSNamespaceRule aNamespaceRule, @NonNull final ICSSVisitor aVisitor) { + aVisitor.onNamespace(aNamespaceRule); } /** - * Visit all declarations contained in the passed declaration container. + * Visit all declarations contained in the given declaration container. * - * @param aHasDeclarations - * The declarations to be visited. May not be null. - * @param aVisitor - * The visitor to be invoked on each declaration. May not be - * null. + * @param aHasDeclarations The declarations to be visited. May not be null. + * @param aVisitor The visitor to be invoked on each declaration. May not be + * null. */ - public static void visitAllDeclarations (@NonNull final IHasCSSDeclarations aHasDeclarations, @NonNull final ICSSVisitor aVisitor) - { + public static void visitAllDeclarations(@NonNull final IHasCSSDeclarations aHasDeclarations, @NonNull final ICSSVisitor aVisitor) { // for all declarations - for (final CSSDeclaration aDeclaration : aHasDeclarations.getAllDeclarations ()) - aVisitor.onDeclaration (aDeclaration); + for (final CSSDeclaration aDeclaration : aHasDeclarations.getAllDeclarations()) + aVisitor.onDeclaration(aDeclaration); + } + + /** + * Visit all nested rules contained in the given rule container. + * + * @param aHasNestedRules The nested rules to be visited. May not be null. + * @param aVisitor The visitor to be invoked on each nested rule. May not be + * null. + */ + public static void visitAllNestedRules(@NonNull final IHasCSSNestedRules aHasNestedRules, @NonNull final ICSSVisitor aVisitor) { + // for all nested rules + for (final ICSSNestedRule aNestedRule : aHasNestedRules.getAllRules()) + visitNestedRule(aNestedRule, aVisitor); } /** * Visit all elements of a single style rule. * - * @param aStyleRule - * The style rule to visit. May not be null. - * @param aVisitor - * The visitor to use. May not be null. + * @param aStyleRule The style rule to visit. May not be null. + * @param aVisitor The visitor to use. May not be null. */ - public static void visitStyleRule (@NonNull final CSSStyleRule aStyleRule, @NonNull final ICSSVisitor aVisitor) - { - aVisitor.onBeginStyleRule (aStyleRule); - try - { + public static void visitStyleRule(@NonNull final CSSStyleRule aStyleRule, @NonNull final ICSSVisitor aVisitor) { + aVisitor.onBeginStyleRule(aStyleRule); + try { // for all selectors - for (final CSSSelector aSelector : aStyleRule.getAllSelectors ()) - aVisitor.onStyleRuleSelector (aSelector); + for (final CSSSelector aSelector : aStyleRule.getAllSelectors()) + aVisitor.onStyleRuleSelector(aSelector); // for all declarations - visitAllDeclarations (aStyleRule, aVisitor); - } - finally - { - aVisitor.onEndStyleRule (aStyleRule); + visitAllDeclarations(aStyleRule, aVisitor); + + // for all nested rules + visitAllNestedRules(aStyleRule, aVisitor); + } finally { + aVisitor.onEndStyleRule(aStyleRule); } } /** * Visit all elements of a single page rule. * - * @param aPageRule - * The page rule to visit. May not be null. - * @param aVisitor - * The visitor to use. May not be null. + * @param aPageRule The page rule to visit. May not be null. + * @param aVisitor The visitor to use. May not be null. */ - public static void visitPageRule (@NonNull final CSSPageRule aPageRule, @NonNull final ICSSVisitor aVisitor) - { - aVisitor.onBeginPageRule (aPageRule); - try - { + public static void visitPageRule(@NonNull final CSSPageRule aPageRule, @NonNull final ICSSVisitor aVisitor) { + aVisitor.onBeginPageRule(aPageRule); + try { // for all declarations - for (final ICSSPageRuleMember aMember : aPageRule.getAllMembers ()) + for (final ICSSPageRuleMember aMember : aPageRule.getAllMembers()) if (aMember instanceof CSSDeclaration) - aVisitor.onDeclaration ((CSSDeclaration) aMember); - else - { + aVisitor.onDeclaration((CSSDeclaration) aMember); + else { final CSSPageMarginBlock aPageMarginBlock = (CSSPageMarginBlock) aMember; - aVisitor.onBeginPageMarginBlock (aPageMarginBlock); - try - { + aVisitor.onBeginPageMarginBlock(aPageMarginBlock); + try { // for all declarations - visitAllDeclarations (aPageMarginBlock, aVisitor); - } - finally - { - aVisitor.onEndPageMarginBlock (aPageMarginBlock); + visitAllDeclarations(aPageMarginBlock, aVisitor); + } finally { + aVisitor.onEndPageMarginBlock(aPageMarginBlock); } } + } finally { + aVisitor.onEndPageRule(aPageRule); } - finally - { - aVisitor.onEndPageRule (aPageRule); + } + + /** + * Visit all elements of a single property rule. + * + * @param aPageRule 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 aPageRule, @NonNull final ICSSVisitor aVisitor) + { + try { + aVisitor.onBeginPropertyRule(aPageRule); + } finally { + aVisitor.onEndPropertyRule(aPageRule); } } @@ -311,6 +320,23 @@ public static void visitLayerRule (@NonNull final CSSLayerRule aLayerRule, @NonN } } + /** + * Visit all elements of a single nested declarations. + * + * @param aNestedDeclarations + * The nested declarations to visit. May not be null. + * @param aVisitor + * The visitor to use. May not be null. + */ + public static void visitNestedDeclarations (@NonNull final CSSNestedDeclarations aNestedDeclarations, @NonNull final ICSSVisitor aVisitor) + { + aVisitor.onBeginNestedDeclarations(aNestedDeclarations); + for (final CSSDeclaration aDeclaration : aNestedDeclarations.getAllDeclarations()) { + aVisitor.onDeclaration(aDeclaration); + } + aVisitor.onEndNestedDeclarations(aNestedDeclarations); + } + /** * Visit all elements of a single unknown @ rule. * @@ -345,42 +371,90 @@ public static void visitTopLevelRule (@NonNull final ICSSTopLevelRule aTopLevelR visitPageRule ((CSSPageRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSFontFaceRule) + if (aTopLevelRule instanceof CSSPropertyRule) { - visitFontFaceRule ((CSSFontFaceRule) aTopLevelRule, aVisitor); + visitPropertyRule ((CSSPropertyRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSMediaRule) + if (aTopLevelRule instanceof CSSFontFaceRule) { - visitMediaRule ((CSSMediaRule) aTopLevelRule, aVisitor); + visitFontFaceRule ((CSSFontFaceRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSKeyframesRule) + if (aTopLevelRule instanceof CSSMediaRule) { - visitKeyframesRule ((CSSKeyframesRule) aTopLevelRule, aVisitor); + visitMediaRule ((CSSMediaRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSViewportRule) + if (aTopLevelRule instanceof CSSKeyframesRule) { - visitViewportRule ((CSSViewportRule) aTopLevelRule, aVisitor); + visitKeyframesRule ((CSSKeyframesRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSSupportsRule) + if (aTopLevelRule instanceof CSSViewportRule) { - visitSupportsRule ((CSSSupportsRule) aTopLevelRule, aVisitor); + visitViewportRule ((CSSViewportRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSLayerRule) + if (aTopLevelRule instanceof CSSSupportsRule) { - visitLayerRule ((CSSLayerRule) aTopLevelRule, aVisitor); + visitSupportsRule ((CSSSupportsRule) aTopLevelRule, aVisitor); } else - if (aTopLevelRule instanceof CSSUnknownRule) + if (aTopLevelRule instanceof CSSLayerRule) { - visitUnknownRule ((CSSUnknownRule) aTopLevelRule, aVisitor); + visitLayerRule ((CSSLayerRule) 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!"); + } + + /** + * Visit all elements of a single {@link ICSSNestedRule nested rule}. + * + * @param aNestedRule + * The nested rule to visit. May not be null. + * @param aVisitor + * The visitor to use. May not be null. + */ + public static void visitNestedRule (@NonNull final ICSSNestedRule aNestedRule, @NonNull final ICSSVisitor aVisitor) + { + if (aNestedRule instanceof CSSStyleRule) + { + visitStyleRule ((CSSStyleRule) aNestedRule, aVisitor); + } + else + if (aNestedRule instanceof CSSMediaRule) + { + visitMediaRule ((CSSMediaRule) aNestedRule, aVisitor); + } + else + if (aNestedRule instanceof CSSSupportsRule) + { + visitSupportsRule ((CSSSupportsRule) aNestedRule, aVisitor); + } + else + if (aNestedRule instanceof CSSLayerRule) + { + visitLayerRule ((CSSLayerRule) aNestedRule, aVisitor); + } + else + if (aNestedRule instanceof CSSNestedDeclarations) + { + visitNestedDeclarations ((CSSNestedDeclarations) aNestedRule, aVisitor); + } + else + if (aNestedRule instanceof CSSUnknownRule) + { + visitUnknownRule ((CSSUnknownRule) aNestedRule, aVisitor); + } + else + throw new IllegalStateException ("Nested rule " + aNestedRule + " is unsupported!"); } /** @@ -421,7 +495,7 @@ public static void visitCSS (@NonNull final CascadingStyleSheet aCSS, @NonNull f } /** - * Visit all items that can contain URLs in CSS files. Therefore the special + * Visit all items that can contain URLs in CSS files. Therefore, the special * visitor class {@link CSSVisitorForUrl} is used. * * @param aCSS @@ -438,7 +512,7 @@ public static void visitCSSUrl (@NonNull final CascadingStyleSheet aCSS, @NonNul } /** - * Visit all items that can contain URLs in CSS files. Therefore the special + * Visit all items that can contain URLs in CSS files. Therefore, the special * visitor class {@link CSSVisitorForUrl} is used. * * @param aCSS diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitorForUrl.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitorForUrl.java index 3d055e50..7caa4e33 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 @@ -175,6 +175,18 @@ public void onEndPageRule (@NonNull final CSSPageRule aPageRule) m_aTopLevelRule.pop (); } + @Override + public void onBeginPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + { + m_aTopLevelRule.push (aPropertyRule); + } + + @Override + public void onEndPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + { + m_aTopLevelRule.pop (); + } + public void onBeginFontFaceRule (@NonNull final CSSFontFaceRule aFontFaceRule) { m_aTopLevelRule.push (aFontFaceRule); @@ -245,6 +257,16 @@ public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule) m_aTopLevelRule.pop(); } + @Override + public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + // no action + } + + @Override + public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + // no action + } + public void onUnknownRule (@NonNull final CSSUnknownRule aUnknownRule) { // no action diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/DefaultCSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/DefaultCSSVisitor.java index d857624b..2cab9be5 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 @@ -16,6 +16,8 @@ */ package com.helger.css.decl.visit; +import com.helger.css.decl.CSSNestedDeclarations; +import com.helger.css.decl.CSSPropertyRule; import org.jspecify.annotations.NonNull; import com.helger.annotation.concurrent.Immutable; @@ -92,6 +94,14 @@ public void onEndPageMarginBlock (@NonNull final CSSPageMarginBlock aPageMarginB public void onEndPageRule (@NonNull final CSSPageRule aPageRule) {} + @Override + public void onBeginPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + {} + + @Override + public void onEndPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + {} + @OverrideOnDemand public void onBeginFontFaceRule (@NonNull final CSSFontFaceRule aFontFaceRule) {} @@ -147,7 +157,15 @@ public void onBeginLayerRule (@NonNull final CSSLayerRule aLayerRule) @OverrideOnDemand public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule) {} - + + @Override + public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) + {} + + @Override + public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) + {} + @OverrideOnDemand public void onUnknownRule (@NonNull final CSSUnknownRule aUnknownRule) {} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java index 002f7d14..794373e3 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 @@ -16,6 +16,8 @@ */ package com.helger.css.decl.visit; +import com.helger.css.decl.CSSNestedDeclarations; +import com.helger.css.decl.CSSPropertyRule; import org.jspecify.annotations.NonNull; import com.helger.css.decl.CSSDeclaration; @@ -139,6 +141,21 @@ public interface ICSSVisitor */ void onEndPageRule (@NonNull CSSPageRule aPageRule); + /** + * 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); + // font face rules: /** * Called when a font-face rule starts.
@@ -262,6 +279,21 @@ public interface ICSSVisitor */ void onEndLayerRule (@NonNull CSSLayerRule aLayerRule); + /** + * Called when a nested declarations rule starts. + * @param aNestedDeclarations + * The nested declarations. Never null. + */ + void onBeginNestedDeclarations (@NonNull CSSNestedDeclarations aNestedDeclarations); + + /** + * Called when a nested declarations rule ends. + * + * @param aNestedDeclarations + * The nested declarations. Never null. + */ + void onEndNestedDeclarations (@NonNull CSSNestedDeclarations aNestedDeclarations); + // unknown rules /** * Called when an unknown rule is encountered. diff --git a/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java b/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java index d34adccc..57d8ca84 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 @@ -16,6 +16,8 @@ */ package com.helger.css.handler; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import org.jspecify.annotations.NonNull; @@ -232,7 +234,8 @@ private ICSSSelectorMember _createSelectorMember (final CSSNode aNode) if (ECSSNodeType.NAMESPACEPREFIX.isNode (aNode) || ECSSNodeType.ELEMENTNAME.isNode (aNode) || ECSSNodeType.HASH.isNode (aNode) || - ECSSNodeType.CLASS.isNode (aNode)) + ECSSNodeType.CLASS.isNode (aNode) || + ECSSNodeType.NESTING.isNode (aNode)) { if (nChildCount != 0) _throwUnexpectedChildrenCount (aNode, "CSS simple selector member expected 0 children and got " + nChildCount); @@ -776,8 +779,91 @@ private void _readStyleDeclarationList (@NonNull final CSSNode aNode, } } + private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aNode, + @NonNull final Consumer aDeclarationConsumer, + @NonNull final Consumer aNestedRuleConsumer, + final int nStyleRuleCount) + { + _expectNodeType (aNode, ECSSNodeType.STYLEDECLARATIONLISTWITHNESTED); + // Read all contained declarations and rules + final int nDecls = aNode.jjtGetNumChildren (); + CSSNestedDeclarations aNestedDeclarations = null; + for (int nDecl = 0; nDecl < nDecls; ++nDecl) + { + final CSSNode aChildNode = aNode.jjtGetChild (nDecl); + if (ECSSNodeType.STYLEDECLARATION.isNode (aChildNode)) + { + final CSSDeclaration aDeclaration = _createDeclaration (aChildNode); + if (aDeclaration != null) { + // declarations that appear at the start are added as declarations of the style rule + // declarations that appear interspersed with other rules are wrapped in a nested declarations element + if (aNestedDeclarations != null) { + aNestedDeclarations.addDeclaration(aDeclaration); + } else { + aDeclarationConsumer.accept (aDeclaration); + } + } + } + else + if (ECSSNodeType.STYLERULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSStyleRule aRule = _createStyleRule (aChildNode, nStyleRuleCount + 1); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSMediaRule aRule = _createMediaRule (aChildNode, nStyleRuleCount); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSSupportsRule aRule = _createSupportsRule (aChildNode, nStyleRuleCount); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSLayerRule aRule = _createLayerRule (aChildNode, nStyleRuleCount); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSUnknownRule aRule = _createUnknownRule (aChildNode); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + // else + // ignore ERROR_SKIP to and all unsupported nested "@" rules + } + // append trailing declarations if there are any + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + } + @Nullable - private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) + private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.STYLERULE); final CSSStyleRule ret = new CSSStyleRule (); @@ -793,14 +879,24 @@ private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) ret.addSelector (_createSelector (aChildNode)); } + else if (ECSSNodeType.RELATIVESELECTOR.isNode (aChildNode)) + { + if (!bSelectors) + m_aErrorHandler.onCSSInterpretationError ("Found a selector after a declaration!"); + + if (nStyleRuleCount == 0) + m_aErrorHandler.onCSSInterpretationError ("Relative selectors are not allowed at the top level!"); + + ret.addSelector (_createRelativeSelector (aChildNode)); + } else { // OK, we're after the selectors bSelectors = false; - if (ECSSNodeType.STYLEDECLARATIONLIST.isNode (aChildNode)) + if (ECSSNodeType.STYLEDECLARATIONLISTWITHNESTED.isNode (aChildNode)) { - // Read all contained declarations - _readStyleDeclarationList (aChildNode, ret::addDeclaration); + // Read all contained declarations and nested rules + _readStyleDeclarationListWithNestedRules (aChildNode, ret::addDeclaration, ret::addRule, nStyleRuleCount); } else if (!ECSSNodeType.isErrorNode (aChildNode)) @@ -881,7 +977,7 @@ private CSSPageRule _createPageRule (@NonNull final CSSNode aNode) } @NonNull - private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) + private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.MEDIARULE); final CSSMediaRule ret = new CSSMediaRule (); @@ -897,7 +993,7 @@ private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) else if (ECSSNodeType.STYLERULE.isNode (aChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aChildNode, nStyleRuleCount); if (aStyleRule != null) ret.addRule (aStyleRule); } @@ -905,7 +1001,7 @@ private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) { // Nested media rules are OK! - ret.addRule (_createMediaRule (aChildNode)); + ret.addRule (_createMediaRule (aChildNode, nStyleRuleCount)); } else if (ECSSNodeType.PAGERULE.isNode (aChildNode)) @@ -921,10 +1017,10 @@ private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) ret.addRule (_createViewportRule (aChildNode)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode)); + ret.addRule (_createSupportsRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode)); + ret.addRule (_createLayerRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) { @@ -1072,7 +1168,7 @@ private CSSFontFaceRule _createFontFaceRule (@NonNull final CSSNode aNode) } @NonNull - private CSSLayerRule _createLayerRule (@NonNull final CSSNode aNode) + private CSSLayerRule _createLayerRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.LAYERRULE); final int nChildCount = aNode.jjtGetNumChildren (); @@ -1110,19 +1206,19 @@ private CSSLayerRule _createLayerRule (@NonNull final CSSNode aNode) final CSSNode aBodyChildNode = aBodyNode.jjtGetChild (nIndex); if (ECSSNodeType.STYLERULE.isNode (aBodyChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aBodyChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aBodyChildNode, nStyleRuleCount); if (aStyleRule != null) ret.addRule (aStyleRule); } else if (ECSSNodeType.LAYERRULE.isNode (aBodyChildNode)) - ret.addRule (_createLayerRule (aBodyChildNode)); + ret.addRule (_createLayerRule (aBodyChildNode, nStyleRuleCount)); else if (ECSSNodeType.MEDIARULE.isNode (aBodyChildNode)) - ret.addRule (_createMediaRule (aBodyChildNode)); + ret.addRule (_createMediaRule (aBodyChildNode, nStyleRuleCount)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aBodyChildNode)) - ret.addRule (_createSupportsRule (aBodyChildNode)); + ret.addRule (_createSupportsRule (aBodyChildNode, nStyleRuleCount)); else if (ECSSNodeType.KEYFRAMESRULE.isNode (aBodyChildNode)) ret.addRule (_createKeyframesRule (aBodyChildNode)); @@ -1229,6 +1325,54 @@ private CSSViewportRule _createViewportRule (@NonNull final CSSNode aNode) return ret; } + @NonNull + private CSSPropertyRule _createPropertyRule (@NonNull final CSSNode aNode) + { + _expectNodeType (aNode, ECSSNodeType.PROPERTYRULE); + + final String sPropertyName = aNode.getText (); + + final CSSPropertyRule ret = new CSSPropertyRule (sPropertyName); + if (m_bUseSourceLocation) + ret.setSourceLocation (aNode.getSourceLocation ()); + + final List aDeclarations = new ArrayList<>(); + for (final CSSNode aChildNode : aNode) + { + if (ECSSNodeType.STYLEDECLARATIONLIST.isNode (aChildNode)) + // Read all contained declarations + _readStyleDeclarationList (aChildNode, aDeclarations::add); + else + if (!ECSSNodeType.isErrorNode (aChildNode)) + m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule child: " + ECSSNodeType.getNodeName (aChildNode)); + } + for (final CSSDeclaration aDeclaration : aDeclarations) + switch (aDeclaration.getProperty()) + { + case "syntax": + ret.setSyntax(aDeclaration.getExpression().getAsCSSString()); + break; + case "inherits": + switch (aDeclaration.getExpression().getAsCSSString()) { + case "true": + ret.setInherits(true); + break; + case "false": + ret.setInherits(false); + break; + default: + } + break; + case "initial-value": + ret.setInitialValue(aDeclaration.getExpression().getAsCSSString()); + break; + default: + m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule declaration: " + aDeclaration.getAsCSSString()); + break; + } + return ret; + } + @NonNull private CSSNamespaceRule _createNamespaceRule (@NonNull final CSSNode aNode) { @@ -1329,7 +1473,7 @@ private ICSSSupportsConditionMember _createSupportsConditionMemberRecursive (@No } @NonNull - private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) + private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.SUPPORTSRULE); final CSSSupportsRule ret = new CSSSupportsRule (); @@ -1349,13 +1493,13 @@ private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) else if (ECSSNodeType.STYLERULE.isNode (aChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aChildNode, nStyleRuleCount); if (aStyleRule != null) ret.addRule (aStyleRule); } else if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) - ret.addRule (_createMediaRule (aChildNode)); + ret.addRule (_createMediaRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.PAGERULE.isNode (aChildNode)) ret.addRule (_createPageRule (aChildNode)); @@ -1370,10 +1514,10 @@ private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) ret.addRule (_createViewportRule (aChildNode)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode)); + ret.addRule (_createSupportsRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode)); + ret.addRule (_createLayerRule (aChildNode, nStyleRuleCount)); else if (!ECSSNodeType.isErrorNode (aChildNode)) m_aErrorHandler.onCSSInterpretationError ("Unsupported supports-rule child: " + @@ -1429,7 +1573,7 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a else if (ECSSNodeType.STYLERULE.isNode (aChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aChildNode, 0); if (aStyleRule != null) ret.addRule (aStyleRule); } @@ -1437,48 +1581,51 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a if (ECSSNodeType.PAGERULE.isNode (aChildNode)) ret.addRule (_createPageRule (aChildNode)); else - if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) - ret.addRule (_createMediaRule (aChildNode)); + if (ECSSNodeType.PROPERTYRULE.isNode (aChildNode)) + ret.addRule (_createPropertyRule (aChildNode)); else - if (ECSSNodeType.FONTFACERULE.isNode (aChildNode)) - ret.addRule (_createFontFaceRule (aChildNode)); + if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) + ret.addRule (_createMediaRule (aChildNode, 0)); else - if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode)); + if (ECSSNodeType.FONTFACERULE.isNode (aChildNode)) + ret.addRule (_createFontFaceRule (aChildNode)); else - if (ECSSNodeType.KEYFRAMESRULE.isNode (aChildNode)) - ret.addRule (_createKeyframesRule (aChildNode)); + if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) + ret.addRule (_createLayerRule (aChildNode, 0)); else - if (ECSSNodeType.VIEWPORTRULE.isNode (aChildNode)) - ret.addRule (_createViewportRule (aChildNode)); + if (ECSSNodeType.KEYFRAMESRULE.isNode (aChildNode)) + ret.addRule (_createKeyframesRule (aChildNode)); else - if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode)); + if (ECSSNodeType.VIEWPORTRULE.isNode (aChildNode)) + ret.addRule (_createViewportRule (aChildNode)); else - if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) - { - // Unknown rule indicates either - // 1. a parsing error - // 2. a non-standard rule - ret.addRule (_createUnknownRule (aChildNode)); - } + if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) + ret.addRule (_createSupportsRule (aChildNode, 0)); else - if (ECSSNodeType.ROOT.isNode (aChildNode)) + if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) { - /* - * In case a parsing error occurs (as e.g. happening in issue #41) - * and browser compliant mode is enabled, some CSS code is skipped - * and a retry happens. This retry will be a recursive stylesheet - * object that is a child of the previous stylesheet but "flattened" - * for the result object. - */ - _recursiveFillCascadingStyleSheetFromNode (aChildNode, ret); + // Unknown rule indicates either + // 1. a parsing error + // 2. a non-standard rule + ret.addRule (_createUnknownRule (aChildNode)); } else - m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + - ECSSNodeType.getNodeName (aNode) + - ": " + - ECSSNodeType.getNodeName (aChildNode)); + if (ECSSNodeType.ROOT.isNode (aChildNode)) + { + /* + * In case a parsing error occurs (as e.g. happening in issue #41) + * and browser compliant mode is enabled, some CSS code is skipped + * and a retry happens. This retry will be a recursive stylesheet + * object that is a child of the previous stylesheet but "flattened" + * for the result object. + */ + _recursiveFillCascadingStyleSheetFromNode (aChildNode, ret); + } + else + m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + + ECSSNodeType.getNodeName (aNode) + + ": " + + ECSSNodeType.getNodeName (aChildNode)); } } diff --git a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java index 40b7591d..95e49ff1 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 @@ -43,6 +43,7 @@ public enum ECSSNodeType STYLERULE (ParserCSS30TreeConstants.JJTSTYLERULE), IMPORTRULE (ParserCSS30TreeConstants.JJTIMPORTRULE), PAGERULE (ParserCSS30TreeConstants.JJTPAGERULE), + PROPERTYRULE (ParserCSS30TreeConstants.JJTPROPERTYRULE), MEDIARULE (ParserCSS30TreeConstants.JJTMEDIARULE), LAYERRULE (ParserCSS30TreeConstants.JJTLAYERRULE), FONTFACERULE (ParserCSS30TreeConstants.JJTFONTFACERULE), @@ -50,12 +51,14 @@ public enum ECSSNodeType SELECTOR (ParserCSS30TreeConstants.JJTSELECTOR), RELATIVESELECTOR (ParserCSS30TreeConstants.JJTRELATIVESELECTOR), STYLEDECLARATIONLIST (ParserCSS30TreeConstants.JJTSTYLEDECLARATIONLIST), + STYLEDECLARATIONLISTWITHNESTED (ParserCSS30TreeConstants.JJTSTYLEDECLARATIONLISTWITHNESTED), STYLEDECLARATION (ParserCSS30TreeConstants.JJTSTYLEDECLARATION), // style rule -- selector NAMESPACEPREFIX (ParserCSS30TreeConstants.JJTNAMESPACEPREFIX), ELEMENTNAME (ParserCSS30TreeConstants.JJTELEMENTNAME), HASH (ParserCSS30TreeConstants.JJTIDSELECTOR), CLASS (ParserCSS30TreeConstants.JJTCLASS), + NESTING (ParserCSS30TreeConstants.JJTNESTING), PSEUDO (ParserCSS30TreeConstants.JJTPSEUDOCLASSSELECTOR), HOST (ParserCSS30TreeConstants.JJTHOST), HOSTCONTEXT (ParserCSS30TreeConstants.JJTHOSTCONTEXT), diff --git a/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java b/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java index c799bce8..205725dd 100644 --- a/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java +++ b/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java @@ -98,9 +98,9 @@ public boolean isValidValue (@Nullable final String sValue) return false; // Check each value - for (final String aPart : aParts) + for (final String sPart : aParts) { - final String sTrimmedPart = aPart.trim (); + final String sTrimmedPart = sPart.trim (); if (!super.isValidValue (sTrimmedPart) && !CSSColorHelper.isColorValue (sTrimmedPart)) return false; } diff --git a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java index 893309af..b9f48852 100644 --- a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java +++ b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java @@ -104,9 +104,9 @@ public boolean isValidValue (@Nullable final String sValue) return false; // Check each value - for (final String aPart : aParts) + for (final String sPart : aParts) { - final String sTrimmedPart = aPart.trim (); + final String sTrimmedPart = sPart.trim (); if (!super.isValidValue (sTrimmedPart) && !CSSColorHelper.isColorValue (sTrimmedPart)) return false; } diff --git a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java index 168a5fd4..c27e4fed 100644 --- a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java +++ b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java @@ -142,9 +142,9 @@ public boolean isValidValue (@Nullable final String sValue) if (aParts.length < m_nMinNumbers || aParts.length > m_nMaxNumbers) return false; - for (final String aPart : aParts) + for (final String sPart : aParts) { - final String sTrimmedPart = aPart.trim (); + final String sTrimmedPart = sPart.trim (); if (!super.isValidValue (sTrimmedPart) && !CSSNumberHelper.isValueWithUnit (sTrimmedPart, m_bWithPercentage)) return false; } diff --git a/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java b/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java index a5dafbab..7c32701f 100644 --- a/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java +++ b/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java @@ -44,7 +44,7 @@ @NotThreadSafe public class CSSWriter { - /** By default optimized output is disabled */ + /** By default, optimized output is disabled */ public static final boolean DEFAULT_OPTIMIZED_OUTPUT = CSSWriterSettings.DEFAULT_OPTIMIZED_OUTPUT; private final CSSWriterSettings m_aSettings; @@ -89,7 +89,7 @@ public CSSWriter (@NonNull final CSSWriterSettings aSettings) } /** - * Check if the header text should be emitted. By default it is enabled, if non-optimized output + * Check if the header text should be emitted. By default, it is enabled, if non-optimized output * is desired. * * @return true if the header text should be emitted, false if not. @@ -100,7 +100,7 @@ public boolean isWriteHeaderText () } /** - * Determine whether the file header should be written or not. By default it is enabled, if + * Determine whether the file header should be written or not. By default, it is enabled, if * non-optimized output is desired. * * @param bWriteHeaderText @@ -126,7 +126,7 @@ public String getHeaderText () /** * Set a custom header text that should be emitted. This text may be multi line separated by the - * '\n' character. It will emitted if {@link #isWriteHeaderText()} returns true. + * '\n' character. It will be emitted if {@link #isWriteHeaderText()} returns true. * * @param sHeaderText * The header text to be emitted. May be null. @@ -140,7 +140,7 @@ public CSSWriter setHeaderText (@Nullable final String sHeaderText) } /** - * Check if the footer text should be emitted. By default it is enabled, if non-optimized output + * Check if the footer text should be emitted. By default, it is enabled, if non-optimized output * is desired. * * @return true if the footer text should be emitted, false if not. @@ -151,7 +151,7 @@ public boolean isWriteFooterText () } /** - * Determine whether the file footer should be written or not. By default it is enabled, if + * Determine whether the file footer should be written or not. By default, it is enabled, if * non-optimized output is desired. * * @param bWriteFooterText @@ -177,7 +177,7 @@ public String getFooterText () /** * Set a custom footer text that should be emitted. This text may be multi line separated by the - * '\n' character. It will emitted if {@link #isWriteFooterText()} returns true. + * '\n' character. It will be emitted if {@link #isWriteFooterText()} returns true. * * @param sFooterText * The footer text to be emitted. May be null. @@ -191,7 +191,7 @@ public CSSWriter setFooterText (@Nullable final String sFooterText) } /** - * @return The current defined content charset for the CSS. By default it is null. + * @return The current defined content charset for the CSS. By default, it is null. */ @Nullable public String getContentCharset () @@ -201,7 +201,7 @@ public String getContentCharset () /** * Define the content charset to be used. If not null and not empty, the - * @charset element is emitted into the CSS. By default no charset is defined.
+ * @charset element is emitted into the CSS. By default, no charset is defined.
* Important: this does not define the encoding of the output - it is just a declarative * marker inside the code. Best practice is to use the same encoding for the CSS and the * respective writer! @@ -235,7 +235,7 @@ public CSSWriterSettings getSettings () * @param aCSS * The CSS to write. May not be null. * @param aWriter - * The write to write the text to. May not be null. Is automatically closed + * The writer to write the text to. May not be null. Is automatically closed * after the writing! * @throws IOException * In case writing fails. @@ -268,32 +268,58 @@ public void writeCSS (@NonNull final CascadingStyleSheet aCSS, @NonNull @WillClo aWriter.write (sNewLineString); } + int nRulesEmitted = 0; + // Charset? Must be the first element before the import if (StringHelper.isNotEmpty (m_sContentCharset)) { aWriter.write ("@charset \"" + m_sContentCharset + "\";"); - if (!bOptimizedOutput) - aWriter.write (sNewLineString); + ++nRulesEmitted; } // Import rules - int nRulesEmitted = 0; + boolean bFirst = true; final ICommonsList aImportRules = aCSS.getAllImportRules (); if (aImportRules.isNotEmpty ()) + { + if (!bOptimizedOutput && nRulesEmitted > 0) + { + aWriter.write(sNewLineString); + aWriter.write(sNewLineString); + } for (final CSSImportRule aImportRule : aImportRules) { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aWriter.write (sNewLineString); aWriter.write (aImportRule.getAsCSSString (m_aSettings)); ++nRulesEmitted; } + } // Namespace rules + bFirst = true; final ICommonsList aNamespaceRules = aCSS.getAllNamespaceRules (); if (aNamespaceRules.isNotEmpty ()) + { + if (!bOptimizedOutput && nRulesEmitted > 0) + { + aWriter.write(sNewLineString); + aWriter.write(sNewLineString); + } for (final CSSNamespaceRule aNamespaceRule : aNamespaceRules) { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aWriter.write (sNewLineString); aWriter.write (aNamespaceRule.getAsCSSString (m_aSettings)); ++nRulesEmitted; } + } // Main CSS rules for (final ICSSTopLevelRule aRule : aCSS.getAllRules ()) @@ -302,13 +328,21 @@ public void writeCSS (@NonNull final CascadingStyleSheet aCSS, @NonNull @WillClo if (StringHelper.isNotEmpty (sRuleCSS)) { if (!bOptimizedOutput && nRulesEmitted > 0) - aWriter.write (sNewLineString); + { + aWriter.write(sNewLineString); + aWriter.write(sNewLineString); + } aWriter.write (sRuleCSS); ++nRulesEmitted; } } + // Newline after all rules + if (!bOptimizedOutput && nRulesEmitted > 0){ + aWriter.write (sNewLineString); + } + // Write file footer if (m_bWriteFooterText && StringHelper.isNotEmpty (m_sFooterText)) { @@ -360,7 +394,7 @@ public String getCSSAsString (@NonNull final CascadingStyleSheet aCSS) * @param aCSS * The CSS to write. May not be null. * @param aWriter - * The write to write the text to. May not be null. Is automatically closed + * The writer to write the text to. May not be null. Is automatically closed * after the writing! * @throws IOException * In case writing fails. diff --git a/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java index 9997b1b8..a8bb5a91 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 @@ -16,6 +16,8 @@ */ package com.helger.css.writer; +import com.helger.css.decl.CSSLayerRule; +import com.helger.css.decl.CSSNestedDeclarations; import org.jspecify.annotations.NonNull; import com.helger.annotation.Nonempty; @@ -41,31 +43,35 @@ public class CSSWriterSettings implements ICSSWriterSettings, ICloneable true to write nested declarations, false to ignore them. + * @return This instance for chaining + * @since 8.2.0 + */ + @NonNull + public final CSSWriterSettings setWriteNestedDeclarations(final boolean bWriteNestedDeclarations) + { + m_bWriteNestedDeclarations = bWriteNestedDeclarations; + return this; + } + public final boolean isWriteFontFaceRules () { return m_bWriteFontFaceRules; @@ -239,6 +270,25 @@ public final CSSWriterSettings setWriteKeyframesRules (final boolean bWriteKeyfr return this; } + @Override + public final boolean isWriteLayerRules () + { + return m_bWriteLayerRules; + } + + /** + * Configures whether {@link CSSLayerRule @layer rules} are written. + * @param bWriteLayerRules true to write layer rules, false to ignore them. + * @return This instance for chaining + * @since 8.2.0 + */ + @NonNull + public final CSSWriterSettings setWriteLayerRules (final boolean bWriteLayerRules) + { + m_bWriteLayerRules = bWriteLayerRules; + return this; + } + public final boolean isWriteMediaRules () { return m_bWriteMediaRules; @@ -263,6 +313,18 @@ public final CSSWriterSettings setWritePageRules (final boolean bWritePageRules) return this; } + public final boolean isWritePropertyRules () + { + return m_bWritePropertyRules; + } + + @NonNull + public final CSSWriterSettings setWritePropertyRules (final boolean bWritePropertyRules) + { + m_bWritePropertyRules = bWritePropertyRules; + return this; + } + public final boolean isWriteViewportRules () { return m_bWriteViewportRules; @@ -320,8 +382,10 @@ public boolean equals (final Object o) m_sIndent.equals (rhs.m_sIndent) && m_bQuoteURLs == rhs.m_bQuoteURLs && m_bWriteNamespaceRules == rhs.m_bWriteNamespaceRules && + m_bWriteNestedDeclarations == rhs.m_bWriteNestedDeclarations && m_bWriteFontFaceRules == rhs.m_bWriteFontFaceRules && m_bWriteKeyframesRules == rhs.m_bWriteKeyframesRules && + m_bWriteLayerRules == rhs.m_bWriteLayerRules && m_bWriteMediaRules == rhs.m_bWriteMediaRules && m_bWritePageRules == rhs.m_bWritePageRules && m_bWriteViewportRules == rhs.m_bWriteViewportRules && @@ -338,8 +402,10 @@ public int hashCode () .append (m_sIndent) .append (m_bQuoteURLs) .append (m_bWriteNamespaceRules) + .append (m_bWriteNestedDeclarations) .append (m_bWriteFontFaceRules) .append (m_bWriteKeyframesRules) + .append (m_bWriteLayerRules) .append (m_bWriteMediaRules) .append (m_bWritePageRules) .append (m_bWriteViewportRules) @@ -357,8 +423,10 @@ public String toString () .append ("Indent", m_sIndent) .append ("QuoteURLs", m_bQuoteURLs) .append ("WriteNamespaceRules", m_bWriteNamespaceRules) + .append ("WriteNestedDeclarations", m_bWriteNestedDeclarations) .append ("WriteFontFaceRules", m_bWriteFontFaceRules) .append ("WriteKeyframesRules", m_bWriteKeyframesRules) + .append ("WriteLayerRules", m_bWriteLayerRules) .append ("WriteMediaRules", m_bWriteMediaRules) .append ("WritePageRules", m_bWritePageRules) .append ("WriteViewportRules", m_bWriteViewportRules) diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index a15a6714..bd9cdbaf 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -202,6 +202,7 @@ TOKEN : | < GREATER: ">" > | < TILDE: "~" > | < DOLLAR: "$" > +| < AMPERSAND: "&" > | < HASH: "#" > | < INCLUDES: "~=" > | < DASHMATCH: "|=" > @@ -214,6 +215,7 @@ TOKEN : | < IMPORT_SYM: "@import" > | < NAMESPACE_SYM: "@namespace" > | < PAGE_SYM: "@page" > +| < PROPERTY_SYM: "@property" > | < TOPLEFTCORNER_SYM: "@top-left-corner" > | < TOPLEFT_SYM: "@top-left" > | < TOPCENTER_SYM: "@top-center" > @@ -571,6 +573,7 @@ try{ | layerRule() | fontfaceRule() | keyframesRule() + | propertyRule() | viewportRule() | supportsRule() | unknownRule() @@ -911,6 +914,11 @@ void typeSelector() #void : {} elementName() } +void nestingSelector() #Nesting : {} +{ + { jjtThis.setText ("&"); } +} + void idSelector() : {} { { jjtThis.setText (token.image); } @@ -1031,6 +1039,11 @@ void relativeSelector() : {} selector () } +void selectorOrRelativeSelector() #void : {} +{ + ( LOOKAHEAD( selectorCombinator() ) relativeSelector() | selector() ) +} + void relativeSelectorList() #void : {} { ( )* @@ -1129,6 +1142,7 @@ void simpleSelectorSequence() #void : {} ( idSelector() | classSelector() | attributeSelector() + | nestingSelector() | pseudoClassSelector() | funcNot() )* @@ -1136,6 +1150,7 @@ void simpleSelectorSequence() #void : {} | ( idSelector() | classSelector() | attributeSelector() + | nestingSelector() | pseudoClassSelector() | funcNot() )+ @@ -1218,6 +1233,7 @@ void styleDeclarationOrRule() #void : {} | pageRule() { errorUnexpectedRule ("@page", "page rule in the middle of a rule-set is not allowed!"); } | fontfaceRule() { errorUnexpectedRule ("@font-face", "font-face rule in the middle of a rule-set is not allowed!"); } | keyframesRule() { errorUnexpectedRule ("@keyframes", "keyframes 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!"); } | 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!"); } | unknownRule() { errorUnexpectedRule ("@", "Unknown rule in the middle of a rule-set is not allowed!"); } @@ -1230,6 +1246,28 @@ void styleDeclarationOrRule() #void : {} ) } +void styleDeclarationOrRuleWithNested() #void : {} +{ + ( LOOKAHEAD( property() ) styleDeclaration() ( LOOKAHEAD( ()* ( | ) ) ()* | ()* ()* ) + | ()* // final semicolon from single line comment + | ( styleRule() + | mediaRule() { } + | pageRule() { errorUnexpectedRule ("@page", "page rule is not allowed as a nested rule!"); } + | fontfaceRule() { errorUnexpectedRule ("@font-face", "font-face rule is not allowed as a nested rule!"); } + | keyframesRule() { errorUnexpectedRule ("@keyframes", "keyframes rule is not allowed as a nested rule!"); } + | propertyRule() { errorUnexpectedRule ("@property", "property rule in the middle of a rule-set is not allowed!"); } + | viewportRule() { errorUnexpectedRule ("@viewport", "viewport rule is not allowed as a nested rule!"); } + | supportsRule() { } + | 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!"); } + | namespaceRule() { errorUnexpectedRule ("@namespace", "namespace rule is not allowed as a nested rule!"); } + | layerRule() { } + ) + ( | | )* + ) +} + CSSNode styleDeclarationList() : {} { try { @@ -1250,12 +1288,42 @@ try { { return jjtThis; } } +CSSNode styleDeclarationListWithNested() : {} +{ +try { + ( )* + ( styleDeclarationOrRuleWithNested() )* +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else { + errorSkipTo (ex, RBRACE); + token_source.backup(1); + } +} + { return jjtThis; } +} + void styleDeclarationBlock() #void : {} { try { styleDeclarationList() +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else + errorSkipTo (ex, RBRACE); +} +} + +void styleDeclarationBlockWithNested() #void : {} +{ + +try { + styleDeclarationListWithNested() + } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); @@ -1267,14 +1335,14 @@ try { void styleRule() : {} { try{ - selector () + selectorOrRelativeSelector () ( )* ( ( )* - selector() + selectorOrRelativeSelector() ( )* )* - styleDeclarationBlock() + styleDeclarationBlockWithNested() } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipInSelector (ex); @@ -1359,6 +1427,7 @@ void mediaRuleList() #void : {} | unknownRule() | 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!"); } + | propertyRule() { errorUnexpectedRule ("@property", "property 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!"); } ) ( )* @@ -1618,6 +1687,19 @@ void viewportRule() : {} styleDeclarationBlock() } +// +// Property rule +// https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule +// +void propertyRule() : {} +{ + + ( )* + { jjtThis.setText (token.image); } + ( )* + styleDeclarationBlock() +} + // // Supports rule // http://www.w3.org/TR/2013/CR-css3-conditional-20130404/#at-supports @@ -1674,6 +1756,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!"); } ) } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSFontFaceRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSFontFaceRuleTest.java new file mode 100644 index 00000000..bd6af985 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/CSSFontFaceRuleTest.java @@ -0,0 +1,48 @@ +package com.helger.css.decl; + +import com.helger.css.reader.CSSReader; +import org.jspecify.annotations.NonNull; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test class for {@link CSSFontFaceRule}. + * + * @author Philip Helger + */ +public class CSSFontFaceRuleTest { + @NonNull + private static CSSFontFaceRule _parse (@NonNull final String sCSS) + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasFontFaceRules ()); + assertEquals (1, aCSS.getFontFaceRuleCount ()); + final CSSFontFaceRule ret = aCSS.getAllFontFaceRules ().get (0); + assertNotNull (ret); + return ret; + } + + @Test + public void testReadUnicodeRangeSingle() { + CSSFontFaceRule aRule = _parse ("@font-face { unicode-range: U+26; }"); + + assertEquals (1, aRule.getDeclarationCount ()); + assertEquals ("unicode-range", aRule.getDeclarationAtIndex(0).getProperty()); + assertEquals ("U+26", aRule.getDeclarationAtIndex(0).getExpression().getAsCSSString()); + } + + @Test + public void testReadUnicodeRangeFromTo() { + CSSFontFaceRule aRule = _parse ("@font-face { unicode-range: U+0025-00FF; }"); + + assertEquals (1, aRule.getDeclarationCount ()); + assertEquals ("unicode-range", aRule.getDeclarationAtIndex(0).getProperty()); + assertEquals ("U+0025-00FF", aRule.getDeclarationAtIndex(0).getExpression().getAsCSSString()); + } +} diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSImportRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSImportRuleTest.java index 1a962e85..17520aed 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSImportRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSImportRuleTest.java @@ -76,8 +76,8 @@ public void testCreate () { final CSSImportRule aImportRule = new CSSImportRule ("a.gif"); final CSSWriterSettings aSettings = new CSSWriterSettings ( false); - assertEquals ("@import url(a.gif);\n", aImportRule.getAsCSSString (aSettings)); + assertEquals ("@import url(a.gif);", aImportRule.getAsCSSString (aSettings)); aSettings.setQuoteURLs (true); - assertEquals ("@import url('a.gif');\n", aImportRule.getAsCSSString (aSettings)); + assertEquals ("@import url('a.gif');", aImportRule.getAsCSSString (aSettings)); } } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSLayerRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSLayerRuleTest.java new file mode 100644 index 00000000..f21d587e --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/CSSLayerRuleTest.java @@ -0,0 +1,68 @@ +package com.helger.css.decl; + +import com.helger.css.reader.CSSReader; +import org.jspecify.annotations.NonNull; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test class for {@link CSSLayerRule}. + * + * @author Philip Helger + */ +public class CSSLayerRuleTest { + @NonNull + private static CSSLayerRule _parse (@NonNull final String sCSS) + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasLayerRules ()); + assertEquals (1, aCSS.getLayerRuleCount ()); + final CSSLayerRule ret = aCSS.getAllLayerRules ().get (0); + assertNotNull (ret); + return ret; + } + + @Test + public void testRead1 () + { + CSSLayerRule aSR = _parse (String.join("\n", List.of( + "@layer state {", + " .foo {", + " color: white;", + " .bar {", + " color: orange", + " }", + " color: black;", + " }", + "}"))); + assertEquals (1, aSR.getSelectorCount()); + assertEquals (1, aSR.getRuleCount ()); + + assertEquals ("state", aSR.getSelectorAtIndex(0)); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (2, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + + assertEquals (".foo", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:white", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".bar", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationCount()); + assertEquals ("color:black", ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationAtIndex(0).getAsCSSString()); + } +} diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSMediaRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSMediaRuleTest.java new file mode 100644 index 00000000..82118947 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/CSSMediaRuleTest.java @@ -0,0 +1,68 @@ +package com.helger.css.decl; + +import com.helger.css.reader.CSSReader; +import org.jspecify.annotations.NonNull; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test class for {@link CSSMediaRule}. + * + * @author Philip Helger + */ +public class CSSMediaRuleTest { + @NonNull + private static CSSMediaRule _parse (@NonNull final String sCSS) + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasMediaRules ()); + assertEquals (1, aCSS.getMediaRuleCount ()); + final CSSMediaRule ret = aCSS.getAllMediaRules ().get (0); + assertNotNull (ret); + return ret; + } + + @Test + public void testRead1 () + { + CSSMediaRule aSR = _parse (String.join("\n", List.of( + "@media print {", + " .foo {", + " color: white;", + " .bar {", + " color: orange", + " }", + " color: black;", + " }", + "}"))); + assertEquals(1, aSR.getMediaQueryCount()); + assertEquals (1, aSR.getRuleCount ()); + + assertEquals("print", aSR.getMediaQueryAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (2, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + + assertEquals (".foo", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:white", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".bar", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationCount()); + assertEquals ("color:black", ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationAtIndex(0).getAsCSSString()); + } +} 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..047eacb5 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -0,0 +1,120 @@ +package com.helger.css.decl; + +import com.helger.css.reader.CSSReader; +import com.helger.css.reader.CSSReaderSettings; +import com.helger.css.utils.CollectingCSSInterpretErrorHandler; +import com.helger.css.writer.CSSWriterSettings; +import org.jspecify.annotations.NonNull; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test class for {@link CSSLayerRule}. + * + * @author Philip Helger + */ +public class CSSPropertyRuleTest { + private final CollectingCSSInterpretErrorHandler m_aPEH = new CollectingCSSInterpretErrorHandler(); + + @NonNull + private CSSPropertyRule _parse (@NonNull final String sCSS) + { + final CSSReaderSettings aSettings = new CSSReaderSettings().setInterpretErrorHandler(m_aPEH); + 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; + } + + @Test + public void testRead1 () + { + CSSPropertyRule aSR = _parse (""" + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + """); + assertEquals ("--rotation", aSR.getName ()); + assertEquals ("\"\"", aSR.getSyntax()); + assertFalse (aSR.isInherits()); + assertEquals ("45deg", aSR.getInitialValue()); + } + + @Test + public void testRead2 () + { + CSSPropertyRule aPR = _parse ("@property --rotation {}"); + assertEquals ("", aPR.getSyntax()); + assertFalse (aPR.isInherits()); + assertEquals ("", aPR.getInitialValue ()); + } + + @Test + public void testRead3 () + { + CSSPropertyRule aPR = _parse ("@property --rotation { syntax: \"\";}"); + assertEquals ("--rotation", aPR.getName ()); + assertEquals ("\"\"", aPR.getSyntax()); + assertFalse (aPR.isInherits()); + assertEquals ("", aPR.getInitialValue ()); + } + + @Test + public void testRead4 () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aPR = _parse ("@property --rotation { color: red;}"); + assertEquals(List.of("Unsupported property rule declaration: color:red"), m_aPEH.getErrors()); + assertEquals ("--rotation", aPR.getName ()); + assertEquals ("", aPR.getSyntax()); + assertFalse (aPR.isInherits()); + assertEquals ("", aPR.getInitialValue ()); + } + + @Test + public void testWrite1 () + { + CSSPropertyRule aPR = _parse (""" + @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 testWrite2 () + { + CSSPropertyRule aPR = _parse ("@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 testWrite3 () + { + CSSPropertyRule aPR = _parse ("@property --rotation {}"); + assertEquals ("@property --rotation {}", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals ("@property --rotation{}", aPR.getAsCSSString (new CSSWriterSettings (true))); + } +} diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java index 58208d43..86f10bb8 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java @@ -20,11 +20,15 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import com.helger.css.reader.CSSReaderSettings; +import com.helger.css.utils.CollectingCSSInterpretErrorHandler; import org.jspecify.annotations.NonNull; import org.junit.Test; import com.helger.css.reader.CSSReader; +import java.util.List; + /** * Test class for class {@link CSSSelector}. * @@ -32,10 +36,13 @@ */ public final class CSSSelectorTest { + private final CollectingCSSInterpretErrorHandler m_aIEH = new CollectingCSSInterpretErrorHandler (); + @NonNull - private static CSSSelector _parse (@NonNull final String sCSS) + private CSSSelector _parse (@NonNull final String sCSS) { - final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + CSSReaderSettings aSettings = new CSSReaderSettings().setInterpretErrorHandler(m_aIEH); + final CascadingStyleSheet aCSS = CSSReader.readFromStringReader (sCSS, aSettings); assertNotNull (sCSS, aCSS); assertTrue (aCSS.hasStyleRules ()); assertEquals (1, aCSS.getStyleRuleCount ()); @@ -48,22 +55,44 @@ private static CSSSelector _parse (@NonNull final String sCSS) return aSel; } + @NonNull + private static CSSStyleRule _parseRule (@NonNull final String sCSS) + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasStyleRules ()); + assertEquals (1, aCSS.getStyleRuleCount ()); + final CSSStyleRule aStyle = aCSS.getAllStyleRules ().get (0); + assertNotNull (sCSS, aStyle); + return aStyle; + } + @Test - public void testRead () + public void testReadElementSelector () { - CSSSelector aSel; - aSel = _parse ("div { color:red }"); - assertEquals (1, aSel.getMemberCount ()); - assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); - assertEquals ("div", aSel.getMemberAtIndex (0).getAsCSSString ()); + CSSSelector aSel = _parse("div { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals(1, aSel.getMemberCount()); + assertTrue(aSel.getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals("div", aSel.getMemberAtIndex(0).getAsCSSString()); + } - aSel = _parse ("#id { color:red }"); - assertEquals (1, aSel.getMemberCount ()); - assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); - assertEquals ("#id", aSel.getMemberAtIndex (0).getAsCSSString ()); - assertEquals ("#id", aSel.getAsCSSString ()); + @Test + public void testReadIdSelector () + { + CSSSelector aSel = _parse("#id { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals(1, aSel.getMemberCount()); + assertTrue(aSel.getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals("#id", aSel.getMemberAtIndex(0).getAsCSSString()); + assertEquals("#id", aSel.getAsCSSString()); + } - aSel = _parse ("#id div { color:red }"); + @Test + public void testReadIdSpaceCombinator () + { + CSSSelector aSel = _parse ("#id div { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (3, aSel.getMemberCount ()); assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertEquals ("#id", aSel.getMemberAtIndex (0).getAsCSSString ()); @@ -72,8 +101,13 @@ public void testRead () assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); assertEquals ("div", aSel.getMemberAtIndex (2).getAsCSSString ()); assertEquals ("#id div", aSel.getAsCSSString ()); + } - aSel = _parse ("#id ~ div { color:red }"); + @Test + public void testReadWaveDashCombinator () + { + CSSSelector aSel = _parse ("#id ~ div { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (3, aSel.getMemberCount ()); assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertEquals ("#id", aSel.getMemberAtIndex (0).getAsCSSString ()); @@ -83,4 +117,113 @@ public void testRead () assertEquals ("div", aSel.getMemberAtIndex (2).getAsCSSString ()); assertEquals ("#id~div", aSel.getAsCSSString ()); } + + @Test + public void testReadNestingSelectorAtStartWithoutSpace() { + CSSSelector aSel = _parse ("&.foo { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); + assertEquals ("&", aSel.getMemberAtIndex (0).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelectorSimpleMember); + assertEquals (".foo", aSel.getMemberAtIndex (1).getAsCSSString ()); + } + + @Test + public void testReadNestingSelectorAtStartWithSpace() { + CSSSelector aSel = _parse ("& .foo { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals (3, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); + assertEquals ("&", aSel.getMemberAtIndex (0).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (1) instanceof ECSSSelectorCombinator); + assertEquals (" ", aSel.getMemberAtIndex (1).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); + assertEquals (".foo", aSel.getMemberAtIndex (2).getAsCSSString ()); + } + + @Test + public void testReadNestingSelectorAtEnd() { + CSSSelector aSel = _parse (".foo & { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals (3, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); + assertEquals (".foo", aSel.getMemberAtIndex (0).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (1) instanceof ECSSSelectorCombinator); + assertEquals (" ", aSel.getMemberAtIndex (1).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); + assertEquals ("&", aSel.getMemberAtIndex (2).getAsCSSString ()); + } + + @Test + public void testReadRelativeSelectorWithinStyleRuleWithWaveDash() { + CSSStyleRule aRule = _parseRule (".foo { ~ .bar { color:red } }"); + + assertEquals(List.of(), m_aIEH.getErrors()); + + assertEquals(".foo", aRule.getSelectorAtIndex(0).getAsCSSString()); + assertTrue(aRule.getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals(1, ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorCount()); + CSSSelector aSel = ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorAtIndex(0); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals ("~", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } + + @Test + public void testReadRelativeSelectorWithinStyleRuleWithPlus() { + CSSStyleRule aRule = _parseRule (".foo { + .bar { color:red } }"); + + assertEquals(List.of(), m_aIEH.getErrors()); + + assertEquals(".foo", aRule.getSelectorAtIndex(0).getAsCSSString()); + assertTrue(aRule.getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals(1, ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorCount()); + CSSSelector aSel = ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorAtIndex(0); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals ("+", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } + + @Test + public void testReadRelativeSelectorWithinStyleRuleWithGreater() { + CSSStyleRule aRule = _parseRule (".foo { > .bar { color:red } }"); + + assertEquals(List.of(), m_aIEH.getErrors()); + + assertEquals(".foo", aRule.getSelectorAtIndex(0).getAsCSSString()); + assertTrue(aRule.getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals(1, ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorCount()); + CSSSelector aSel = ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorAtIndex(0); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals (">", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } + + @Test + public void testReadRelativeSelectorAtTopLevel() { + CSSSelector aSel = _parse ("> .bar { color:red }"); + + assertEquals(List.of("Relative selectors are not allowed at the top level!"), m_aIEH.getErrors()); + + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals (">", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } } 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 2e0ad226..6d3839f0 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 @@ -111,4 +111,182 @@ public void testRead2 () ECSSAttributeCase.CASE_INSENSITIVE))); TestHelper.testDefaultImplementationWithEqualContentObject (aSR, aCreated); } + + @Test + public void testRead3 () + { + CSSStyleRule aSR = _parse ("div { p { color: red; } }"); + assertEquals (1, aSR.getSelectorCount ()); + assertEquals (0, aSR.getDeclarationCount ()); + assertEquals (1, aSR.getRuleCount ()); + + assertEquals("div", aSR.getSelectorAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + assertEquals ("p", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:red", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + } + + @Test + public void testRead4 () + { + CSSStyleRule aSR = _parse (""" + div { + color: red; + p: dummy; + p { + color: dummy; + } + .foobar { + color: green; + #id { + color: blue + } + color: white + } + color: yellow; + @media print { + .print { + color: black; + &:hover { + color: orange; + font-size: 20px; + } + } + } + @layer state { + .alert { + background-color: brown; + p { + border: medium solid limegreen; + } + } + } + }"""); + assertEquals (2, aSR.getDeclarationCount ()); + assertEquals (5, aSR.getRuleCount ()); + + assertEquals ("color:red", aSR.getDeclarationAtIndex(0).getAsCSSString()); + assertEquals ("p:dummy", aSR.getDeclarationAtIndex(1).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + assertEquals ("p", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:dummy", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (1) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (1)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (1)).getDeclarationCount()); + assertEquals (2, ((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleCount()); + assertEquals (".foobar", ((CSSStyleRule)aSR.getRuleAtIndex (1)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:green", ((CSSStyleRule)aSR.getRuleAtIndex (1)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0)).getRuleCount()); + assertEquals ("color:blue", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(1)).getDeclarationCount()); + assertEquals ("color:white", ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(1)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (2) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (2)).getDeclarationCount()); + assertEquals ("color:yellow", ((CSSNestedDeclarations)aSR.getRuleAtIndex (2)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (3) instanceof CSSMediaRule); + assertEquals (1, ((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleCount()); + assertTrue (((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (1, ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".print", ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:black", ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (2, ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals ("&:hover", ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertEquals ("font-size:20px", ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationAtIndex(1).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (4) instanceof CSSLayerRule); + assertEquals (1, ((CSSLayerRule)aSR.getRuleAtIndex (4)).getSelectorCount()); + assertEquals (1, ((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleCount()); + assertEquals ("state", ((CSSLayerRule)aSR.getRuleAtIndex (4)).getSelectorAtIndex(0)); + assertTrue (((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (1, ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".alert", ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("background-color:brown", ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals ("p", ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("border:medium solid limegreen", ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + } + + @Test + public void testRead5 () + { + CSSStyleRule aSR = _parse (""" + div { + color: red; + .a1 { color: green; } + color: blue; + .a2 { color: orange; } + color: yellow; + .a3 { color: white; } + color: cyan; + } + """); + assertEquals (1, aSR.getSelectorCount ()); + assertEquals (1, aSR.getDeclarationCount ()); + assertEquals (6, aSR.getRuleCount ()); + + assertEquals ("div", aSR.getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:red", aSR.getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + assertEquals (".a1", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:green", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (1)).getDeclarationCount()); + assertEquals ("color:blue", ((CSSNestedDeclarations)aSR.getRuleAtIndex (1)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (2) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (2)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (2)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (2)).getRuleCount()); + assertEquals (".a2", ((CSSStyleRule)aSR.getRuleAtIndex (2)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)aSR.getRuleAtIndex (2)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (3) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (3)).getDeclarationCount()); + assertEquals ("color:yellow", ((CSSNestedDeclarations)aSR.getRuleAtIndex (3)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (4) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (4)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (4)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (4)).getRuleCount()); + assertEquals (".a3", ((CSSStyleRule)aSR.getRuleAtIndex (4)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:white", ((CSSStyleRule)aSR.getRuleAtIndex (4)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (5) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (5)).getDeclarationCount()); + assertEquals ("color:cyan", ((CSSNestedDeclarations)aSR.getRuleAtIndex (5)).getDeclarationAtIndex(0).getAsCSSString()); + } } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java index e8b6e735..b66b510c 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java @@ -28,6 +28,8 @@ import com.helger.css.reader.CSSReader; import com.helger.unittest.support.TestHelper; +import java.util.List; + /** * Test class for {@link CSSSupportsRule}. * @@ -128,4 +130,43 @@ public void testRead2 () .addDeclaration ("color", CSSExpression.createSimple ("red"), false)); TestHelper.testDefaultImplementationWithEqualContentObject (aSR, aCreated); } + + @Test + public void testRead3 () + { + CSSSupportsRule aSR = _parse (String.join("\n", List.of( + "@supports(column-count: 1) {", + " .foo {", + " color: white;", + " .bar {", + " color: orange", + " }", + " color: black;", + " }", + "}"))); + + assertEquals (1, aSR.getSupportsConditionMemberCount ()); + assertEquals (1, aSR.getRuleCount ()); + + assertEquals ("(column-count:1)", aSR.getSupportsConditionMemberAtIndex (0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (2, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + + assertEquals (".foo", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals (".foo", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".bar", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationCount()); + assertEquals ("color:black", ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationAtIndex(0).getAsCSSString()); + } } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/CSSVisitorFuncTest.java b/ph-css/src/test/java/com/helger/css/decl/visit/CSSVisitorFuncTest.java index 1346dbd8..1e917898 100644 --- a/ph-css/src/test/java/com/helger/css/decl/visit/CSSVisitorFuncTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/visit/CSSVisitorFuncTest.java @@ -21,7 +21,9 @@ import java.io.File; import java.nio.charset.StandardCharsets; +import java.util.List; +import com.helger.css.decl.CSSStyleRule; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -105,4 +107,55 @@ public void testVisitConstantCSS () CSSVisitor.visitCSSUrl (aCSS, aVisitor); assertEquals (8, aVisitor.getCount ()); } + + @Test + public void testVisitNestedDeclarations() { + CascadingStyleSheet aSheet = CSSReader.readFromString (".foo { color: red; .bar { color: green; } color: blue; }"); + CSSStyleRule aStyleRule = aSheet.getStyleRuleAtIndex (0); + MockCountingNestedDeclarationsVisitor aVisitor = new MockCountingNestedDeclarationsVisitor (); + CSSVisitor.visitStyleRule (aStyleRule, aVisitor); + assertEquals(1, aVisitor.getBeginNestedDeclarationsCount ()); + assertEquals(1, aVisitor.getEndNestedDeclarationsCount ()); + assertEquals(List.of("color:blue;"), aVisitor.getNestedDeclarations ()); + } + + @Test + public void testVisitDeclarations() { + CascadingStyleSheet aSheet = CSSReader.readFromString (".foo { color: red; .bar { color: green; } color: blue; }"); + CSSStyleRule aStyleRule = aSheet.getStyleRuleAtIndex (0); + MockCountingDeclarationsVisitor aVisitor = new MockCountingDeclarationsVisitor (); + CSSVisitor.visitStyleRule (aStyleRule, aVisitor); + assertEquals(3, aVisitor.getDeclarationCount ()); + assertEquals(List.of ("color:red", "color:green", "color:blue"), aVisitor.getDeclarations ()); + } + + @Test + public void testVisitPropertyRule() { + CascadingStyleSheet aSheet = CSSReader.readFromString (""" + @property --canBeAnything { + syntax: "*"; + inherits: true; + } + + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + + @property --defaultSize { + syntax: " | "; + inherits: true; + initial-value: 200px; + } + """); + MockCountingPageRuleVisitor aVisitor = new MockCountingPageRuleVisitor (); + CSSVisitor.visitCSS (aSheet, aVisitor); + assertEquals(3, aVisitor.getBeginPropertyRuleCount ()); + assertEquals(3, aVisitor.getEndPropertyRuleCount ()); + assertEquals(List.of ( + "@property --canBeAnything {\n syntax:\"*\";\n inherits:true;\n}", + "@property --rotation {\n syntax:\"\";\n inherits:false;\n initial-value:45deg;\n}", + "@property --defaultSize {\n syntax:\" | \";\n inherits:true;\n initial-value:200px;\n}"), aVisitor.getPropertyRules ()); + } } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java new file mode 100644 index 00000000..e4d3c1a3 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java @@ -0,0 +1,26 @@ +package com.helger.css.decl.visit; + +import com.helger.css.decl.CSSDeclaration; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +class MockCountingDeclarationsVisitor extends DefaultCSSVisitor { + private int m_nDeclaration = 0; + private final List m_sDeclarations = new ArrayList<>(); + + @Override + public void onDeclaration(@NonNull CSSDeclaration aDeclaration) { + m_nDeclaration++; + m_sDeclarations.add(aDeclaration.getAsCSSString()); + } + + public int getDeclarationCount() { + return m_nDeclaration; + } + + public List getDeclarations() { + return m_sDeclarations; + } +} diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java new file mode 100644 index 00000000..f1f26709 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java @@ -0,0 +1,36 @@ +package com.helger.css.decl.visit; + +import com.helger.css.decl.CSSNestedDeclarations; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +class MockCountingNestedDeclarationsVisitor extends DefaultCSSVisitor { + private int m_nBeginNestedDeclarations = 0; + private int m_nEndNestedDeclarations = 0; + private final List m_sNestedDeclaration = new ArrayList<>(); + + @Override + public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + m_nBeginNestedDeclarations++; + } + + @Override + public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + m_nEndNestedDeclarations++; + m_sNestedDeclaration.add(aNestedDeclarations.getAsCSSString()); + } + + public int getBeginNestedDeclarationsCount() { + return m_nBeginNestedDeclarations; + } + + public int getEndNestedDeclarationsCount() { + return m_nEndNestedDeclarations; + } + + public List getNestedDeclarations() { + return m_sNestedDeclaration; + } +} diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java new file mode 100644 index 00000000..0a6c4e68 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java @@ -0,0 +1,38 @@ +package com.helger.css.decl.visit; + +import com.helger.css.decl.CSSPropertyRule; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +class MockCountingPageRuleVisitor extends DefaultCSSVisitor { + private int m_nBeginPropertyRules = 0; + private int m_nEndPropertyRules = 0; + private final List m_sPropertyRules = new ArrayList<>(); + + @Override + public void onBeginPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + { + m_nBeginPropertyRules++; + m_sPropertyRules.add(aPropertyRule.getAsCSSString()); + } + + @Override + public void onEndPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + { + m_nEndPropertyRules++; + } + + public int getBeginPropertyRuleCount() { + return m_nBeginPropertyRules; + } + + public int getEndPropertyRuleCount() { + return m_nEndPropertyRules; + } + + public List getPropertyRules() { + return m_sPropertyRules; + } +} diff --git a/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java b/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java index 18e8b0b3..c350960b 100644 --- a/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java +++ b/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java @@ -354,9 +354,10 @@ public void testSpecialCasesAsString () // Parsing problem String sCSS = ".class{color:red;.class{color:green}.class{color:blue}"; aCSS = CSSReader.readFromStringReader (sCSS, aReaderSettings); - assertNotNull (aCSS); - assertEquals (bBrowserCompliantMode ? "" : ".class{color:red}.class{color:blue}", - new CSSWriter (aWriterSettings).getCSSAsString (aCSS)); + if (bBrowserCompliantMode) + assertEquals ("", new CSSWriter (aWriterSettings).getCSSAsString (aCSS)); + else + assertNull (aCSS); sCSS = " \n/* comment */\n \n.class{color:red;}"; aCSS = CSSReader.readFromStringReader (sCSS, aReaderSettings); diff --git a/ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java b/ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java new file mode 100644 index 00000000..5105a3f8 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java @@ -0,0 +1,38 @@ +package com.helger.css.utils; + +import com.helger.css.reader.errorhandler.ICSSInterpretErrorHandler; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public final class CollectingCSSInterpretErrorHandler implements ICSSInterpretErrorHandler { + private static final Logger LOGGER = LoggerFactory.getLogger (CollectingCSSInterpretErrorHandler.class); + + private final List m_aWarnings = new ArrayList<>(); + private final List m_aErrors = new ArrayList<>(); + + @Override + public void onCSSInterpretationWarning(@NonNull String sMessage) { + m_aWarnings.add(sMessage); + LOGGER.warn (sMessage); + } + + @Override + public void onCSSInterpretationError(@NonNull String sMessage) { + m_aErrors.add(sMessage); + LOGGER.error (sMessage); + } + + @NonNull + public List getWarnings() { + return List.copyOf(m_aWarnings); + } + + @NonNull + public List getErrors() { + return List.copyOf(m_aErrors); + } +} diff --git a/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java b/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java index 0959b6cc..892efb87 100644 --- a/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java +++ b/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java @@ -17,10 +17,12 @@ package com.helger.css.writer; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import com.helger.base.system.ENewLineMode; import org.jspecify.annotations.NonNull; import org.junit.Test; @@ -36,19 +38,22 @@ public final class CSSWriterSettingsTest extends AbstractCSSTestCase { private static void _checkDefault (@NonNull final ICSSWriterSettings aSettings) { - assertTrue (CSSWriterSettings.DEFAULT_OPTIMIZED_OUTPUT == aSettings.isOptimizedOutput ()); - assertTrue (CSSWriterSettings.DEFAULT_REMOVE_UNNECESSARY_CODE == aSettings.isRemoveUnnecessaryCode ()); - assertSame (CSSWriterSettings.DEFAULT_NEW_LINE_MODE, aSettings.getNewLineMode ()); - assertEquals (CSSWriterSettings.DEFAULT_INDENT, aSettings.getIndent (1)); - assertTrue (CSSWriterSettings.DEFAULT_QUOTE_URLS == aSettings.isQuoteURLs ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_NAMESPACE_RULES == aSettings.isWriteNamespaceRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_FONT_FACE_RULES == aSettings.isWriteFontFaceRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_KEYFRAMES_RULES == aSettings.isWriteKeyframesRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_MEDIA_RULES == aSettings.isWriteMediaRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_PAGE_RULES == aSettings.isWritePageRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_VIEWPORT_RULES == aSettings.isWriteViewportRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_SUPPORTS_RULES == aSettings.isWriteSupportsRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_UNKNOWN_RULES == aSettings.isWriteUnknownRules ()); + assertFalse (aSettings.isOptimizedOutput ()); + assertFalse (aSettings.isRemoveUnnecessaryCode ()); + assertSame (ENewLineMode.UNIX, aSettings.getNewLineMode ()); + assertEquals (" ", aSettings.getIndent (1)); + assertFalse (aSettings.isQuoteURLs ()); + assertTrue (aSettings.isWriteNamespaceRules ()); + assertTrue (aSettings.isWriteNestedDeclarations ()); + assertTrue (aSettings.isWriteFontFaceRules ()); + assertTrue (aSettings.isWriteKeyframesRules ()); + assertTrue (aSettings.isWriteLayerRules ()); + assertTrue (aSettings.isWriteMediaRules ()); + assertTrue (aSettings.isWritePageRules ()); + assertTrue (aSettings.isWritePropertyRules ()); + assertTrue (aSettings.isWriteViewportRules ()); + assertTrue (aSettings.isWriteSupportsRules ()); + assertTrue (aSettings.isWriteUnknownRules ()); } @Test diff --git a/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java b/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java index aa51271f..f2176f83 100644 --- a/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java +++ b/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java @@ -26,6 +26,8 @@ import com.helger.css.decl.CascadingStyleSheet; import com.helger.css.reader.CSSReader; +import java.util.List; + /** * Test class for class {@link CSSWriter}. * @@ -394,4 +396,446 @@ public void testWriteCertainRules () assertNotNull (aCSS); assertEquals ("h1{color:red;margin:1px}h2{color:red;margin:1px}", aWriter.getCSSAsString (aCSS)); } + + @Test + public void testAllRulesWithPrettyPrinting () + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (""" + @charset "UTF-8"; + @import url("x.css"); + @import url("y.css"); + @namespace url(http://www.w3.org/1999/xhtml); + @namespace svg url(http://www.w3.org/2000/svg); + div { + color: red; + p: dummy; + div {} + p { + color: dummy; + } + span { + color: dummy; + margin: 0; + } + .foobar { + color: green; + element {} + #id { + color: blue + } + .class { + color: blue; + padding: 0; + } + color: white + } + color: yellow; + background-color: purple; + font-size: 12px; + @media print {} + @media print { + .print { + color: white; + } + } + @media print { + .print { + color: black; + &:hover { + color: orange; + font-size: 20px; + } + } + .pretty-print { + color: pink; + } + } + @layer state; + @layer state { .alert { color: green; } } + @layer state { + .alert { + background-color: brown; + p { + border: medium solid limegreen; + } + } + .warning { + background-color: red; + } + } + } + @font-face {} + @font-face { font-family: x; } + @font-face { + font-family: x; + src: url(x.woff2) format("woff2"); + } + @keyframes anim1 {} + @keyframes anim2 { from { opacity: 0.5; }} + @keyframes anim3 { from { opacity: 0; } to { opacity: 1; font-size: 12px; } } + @supports (display: grid) {} + @supports (display: grid) { .grid { display: grid; gap: 10px; } } + @supports (display: grid) { + .grid { + display: grid; + } + .grid { + gap: 10px; + } + } + @page {} + @page :first {} + @page :first { margin: 0; } + @page :first { + margin: 0; + padding: 0; + } + @page :first { + @top-center { content: "Preliminary edition" } + } + @page :first { + @bottom-left { } + @top-center { content: "Preliminary edition" } + @bottom-center { content: counter(page); color: violet; } + } + @viewport {} + @viewport { width: device-width; } + @viewport { + width: device-width; + height: device-height; + } + @property --rotation {} + @property --rotation { + syntax: ""; + } + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + @unknown {} + @unknown { a: b; } + @unknown { a: b; c: d; } + @unknown { + a: b; + c: d; + } + @unknown { .foo { a: b; c: d; } }"""); + + assertNotNull (aCSS); + final CSSWriterSettings aSettings = new CSSWriterSettings ().setOptimizedOutput (false); + final String sPrinted = new CSSWriter (aSettings).setFooterText("end-of-file").setContentCharset ("utf-8").getCSSAsString (aCSS); + + assertEquals (""" + /* + * THIS FILE IS GENERATED - DO NOT EDIT + */ + @charset "utf-8"; + + @import url(x.css); + @import url(y.css); + + @namespace url(http://www.w3.org/1999/xhtml); + @namespace svg url(http://www.w3.org/2000/svg); + + div { + color:red; + p:dummy; + + div {} + + p { color:dummy; } + + span { + color:dummy; + margin:0; + } + + .foobar { + color:green; + + element {} + + #id { color:blue; } + + .class { + color:blue; + padding:0; + } + + color:white; + } + + color:yellow; + background-color:purple; + font-size:12px; + + @media print {} + + @media print { + .print { color:white; } + } + + @media print { + .print { + color:black; + + &:hover { + color:orange; + font-size:20px; + } + } + + .pretty-print { color:pink; } + } + + @layer state; + + @layer state { + .alert { color:green; } + } + + @layer state { + .alert { + background-color:brown; + + p { border:medium solid limegreen; } + } + .warning { background-color:red; } + } + } + + @font-face {} + + @font-face { font-family:x; } + + @font-face { + font-family:x; + src:url(x.woff2) format("woff2"); + } + + @keyframes anim1 {} + + @keyframes anim2 { + from { opacity:0.5; } + } + + @keyframes anim3 { + from { opacity:0; } + to { + opacity:1; + font-size:12px; + } + } + + @supports (display:grid) {} + + @supports (display:grid) { + .grid { + display:grid; + gap:10px; + } + } + + @supports (display:grid) { + .grid { display:grid; } + .grid { gap:10px; } + } + + @page {} + + @page :first {} + + @page :first { margin:0; } + + @page :first { + margin:0; + padding:0; + } + + @page :first { @top-center { content:"Preliminary edition"; } } + + @page :first { + @bottom-left {} + @top-center { content:"Preliminary edition"; } + @bottom-center { + content:counter(page); + color:violet; + } + } + + @viewport {} + + @viewport { width:device-width; } + + @viewport { + width:device-width; + height:device-height; + } + + @property --rotation {} + + @property --rotation { syntax:""; } + + @property --rotation { + syntax:""; + inherits:false; + initial-value:45deg; + } + + @unknown {} + + @unknown { + a: b; + } + + @unknown { + a: b; c: d; + } + + @unknown { + a: b; + c: d; + } + + @unknown { + .foo { a: b; c: d; } + } + /* + * end-of-file + */ + """, sPrinted); + } + + @Test + public void testAllRulesWithOptimizedPrinting () + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (""" + @charset "UTF-8"; + @import url("x.css"); + @import url("y.css"); + @namespace url(http://www.w3.org/1999/xhtml); + @namespace svg url(http://www.w3.org/2000/svg); + div { + color: red; + p: dummy; + div {} + p { + color: dummy; + } + span { + color: dummy; + margin: 0; + } + .foobar { + color: green; + element {} + #id { + color: blue + } + .class { + color: blue; + padding: 0; + } + color: white + } + color: yellow; + background-color: purple; + font-size: 12px; + @media print {} + @media print { + .print { + color: white; + } + } + @media print { + .print { + color: black; + &:hover { + color: orange; + font-size: 20px; + } + } + .pretty-print { + color: pink; + } + } + @layer state; + @layer state { .alert { color: green; } } + @layer state { + .alert { + background-color: brown; + p { + border: medium solid limegreen; + } + } + .warning { + background-color: red; + } + } + } + @font-face {} + @font-face { font-family: x; } + @font-face { + font-family: x; + src: url(x.woff2) format("woff2"); + } + @keyframes anim1 {} + @keyframes anim2 { from { opacity: 0.5; }} + @keyframes anim3 { from { opacity: 0; } to { opacity: 1; font-size: 12px; } } + @supports (display: grid) {} + @supports (display: grid) { .grid { display: grid; gap: 10px; } } + @supports (display: grid) { + .grid { + display: grid; + } + .grid { + gap: 10px; + } + } + @page {} + @page :first {} + @page :first { margin: 0; } + @page :first { + margin: 0; + padding: 0; + } + @page :first { + @top-center { content: "Preliminary edition" } + } + @page :first { + @bottom-left { } + @top-center { content: "Preliminary edition" } + @bottom-center { content: counter(page); color: violet; } + } + @viewport {} + @viewport { width: device-width; } + @viewport { + width: device-width; + height: device-height; + } + @property --rotation {} + @property --rotation { + syntax: ""; + } + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + @unknown {} + @unknown { a: b; } + @unknown { a: b; c: d; } + @unknown { + a: b; + c: d; + } + @unknown { .foo { a: b; c: d; } }"""); + + assertNotNull (aCSS); + final CSSWriterSettings aSettings = new CSSWriterSettings ().setOptimizedOutput (true); + final String sPrinted = new CSSWriter (aSettings).setFooterText("end-of-file").setContentCharset ("utf-8").getCSSAsString (aCSS); + + assertEquals ("@charset \"utf-8\";@import url(x.css);@import url(y.css);@namespace url(http://www.w3.org/1999/xhtml);@namespace svg url(http://www.w3.org/2000/svg);div{color:red;p:dummy;div{}p{color:dummy}span{color:dummy;margin:0}.foobar{color:green;element{}#id{color:blue}.class{color:blue;padding:0}color:white}color:yellow;background-color:purple;font-size:12px;@media print{}@media print{.print{color:white}}@media print{.print{color:black;&:hover{color:orange;font-size:20px}}.pretty-print{color:pink}}@layer state;@layer state{.alert{color:green}}@layer state{.alert{background-color:brown;p{border:medium solid limegreen}}.warning{background-color:red}}}@font-face{}@font-face{font-family:x}@font-face{font-family:x;src:url(x.woff2) format(\"woff2\")}@keyframes anim1{}@keyframes anim2{from{opacity:0.5}}@keyframes anim3{from{opacity:0}to{opacity:1;font-size:12px}}@supports (display:grid){}@supports (display:grid){.grid{display:grid;gap:10px}}@supports (display:grid){.grid{display:grid}.grid{gap:10px}}@page{}@page :first{}@page :first{margin:0}@page :first{margin:0;padding:0}@page :first{@top-center{content:\"Preliminary edition\"}}@page :first{@bottom-left{}@top-center{content:\"Preliminary edition\"}@bottom-center{content:counter(page);color:violet}}@viewport{}@viewport{width:device-width}@viewport{width:device-width;height:device-height}@property --rotation{}@property --rotation{syntax:\"\"}@property --rotation{syntax:\"\";inherits:false;initial-value:45deg}@unknown{}@unknown{a: b;}@unknown{a: b; c: d;}@unknown{a: b;\n c: d;}@unknown{.foo { a: b; c: d; }}", sPrinted); + } } diff --git a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css index 3383020e..0e811325 100644 --- a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css +++ b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css @@ -15,4 +15,4 @@ * limitations under the License. */ p { background: lime; } - foo & address, p { background: red; } + foo % address, p { background: red; } diff --git a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css index 4fe7dd32..2895b911 100644 --- a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css +++ b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css @@ -14,5 +14,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - foo & address, p { background: red; } + foo % address, p { background: red; } p { background: lime; } diff --git a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css index 36003477..cc021182 100644 --- a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css +++ b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css @@ -14,5 +14,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - foo & address, p { background: red ! important; } + foo % address, p { background: red ! important; } p { background: lime; } diff --git a/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css b/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css index 92fb50a7..0253ccba 100644 --- a/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css +++ b/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css @@ -407,4 +407,4 @@ solid #fff}#contactstable p{font-size:18px;padding:0px 0px 20px 0px;margin:0px}#contactstable h4{font-size:20px;font-weight:bold;padding:0px -0px 5px 0px;margin:0px} +0px 5px 0px;margin:0px}}