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
* Example:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ * Example:
+ *
+ *
* Marker interface for all top level CSS elements that can occur in any order
- *
+ *
* 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.
- * 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..419b5752 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,16 @@
*/
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.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 +83,53 @@ 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 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..bc0c9bc9 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,58 @@ public ICommonsList 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 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.
+ *
+ * @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..69ebccbb 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.
+ *
+ *
- *
+}
* @author Philip Helger
*/
@NotThreadSafe
@@ -161,14 +162,17 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn
if (!aSettings.isWriteKeyframesRules ())
return "";
+ boolean bFirst = true;
+
if (aSettings.isRemoveUnnecessaryCode () && m_aBlocks.isEmpty ())
return "";
+ final int nBlockCount = m_aBlocks.size ();
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 @keyframes identifier {
0% { top: 0; left: 0; }
30% { top: 50px; }
- }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 @media rule: a list of style rules only valid for certain
- * media.
- * Example:
- * @media print {
+ * media.
+ *
+ *
+}
*
* @author Philip Helger
*/
@NotThreadSafe
-public class CSSMediaRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware
+public class CSSMediaRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware
{
private final ICommonsList @media print {
div#footer {
display: none;
}
-}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:
+ *
+ *
+ *
+ * @author Philip Helger
+ * @since 8.2.0
+ */
+@NotThreadSafe
+public class CSSNestedDeclarations implements ICSSNestedRule, IHasCSSDeclarations div { ... } block
+ *
+ *
+ * color: red;
+ *
+ * span { color: green; }
+ * color: blue;@page rule.
- * Example:
- * @page {
+ * Represents a single
+}
*
* @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/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 @page rule.
+ * @page {
size: auto;
margin: 10%;
-}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..4d41c586 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).
+ *
+ * div {
+ color: red;
+ &:hover {
+ color: blue;
+ }
+}
*
* @author Philip Helger
*/
@NotThreadSafe
-public class CSSStyleRule implements ICSSTopLevelRule, IHasCSSDeclarations 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)
{
@@ -239,20 +324,96 @@ public String getSelectorsAsCSSString (@NonNull final ICSSWriterSettings aSettin
@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 ();
}
@@ -275,13 +436,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 +451,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}
+ *
+ *
+}
*
* @author Philip Helger
*/
@NotThreadSafe
-public class CSSSupportsRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware
+public class CSSSupportsRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware
{
private final ICommonsList @supports (transition-property: color) {
div { color:red; }
-}@viewport rule.
- * Example:
- * @viewport { width: device-width; }
+ * Represents a single @viewport rule.
+ *
+ * @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..f0339db7 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 != nDeclCount - 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
+ *
+ *
+ *
+ *
+ * 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..3e983546 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,27 @@
import com.helger.css.ICSSWriteable;
/**
- *
- *
- * 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
+ ICommonsListnull.
@@ -100,6 +103,22 @@ public static void visitAllDeclarations (@NonNull final IHasCSSDeclarations >
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.
*
@@ -119,6 +138,9 @@ public static void visitStyleRule (@NonNull final CSSStyleRule aStyleRule, @NonN
// for all declarations
visitAllDeclarations (aStyleRule, aVisitor);
+
+ // for all nested rules
+ visitAllNestedRules (aStyleRule, aVisitor);
}
finally
{
@@ -311,6 +333,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.
*
@@ -383,6 +422,49 @@ public static void visitTopLevelRule (@NonNull final ICSSTopLevelRule aTopLevelR
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!");
+ }
+
/**
* Visit all CSS elements in the order of their declaration. import rules come
* first, namespace rules come next and all other top-level rules in the order
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..1feeb669 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
@@ -245,6 +245,14 @@ public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule)
m_aTopLevelRule.pop();
}
+ public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) {
+ // no action
+ }
+
+ 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..0380c78d 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,7 @@
*/
package com.helger.css.decl.visit;
+import com.helger.css.decl.CSSNestedDeclarations;
import org.jspecify.annotations.NonNull;
import com.helger.annotation.concurrent.Immutable;
@@ -147,7 +148,15 @@ public void onBeginLayerRule (@NonNull final CSSLayerRule aLayerRule)
@OverrideOnDemand
public void onEndLayerRule (@NonNull final CSSLayerRule aLayerRule)
{}
-
+
+ @OverrideOnDemand
+ public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations)
+ {}
+
+ @OverrideOnDemand
+ 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..3d5faaae 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,7 @@
*/
package com.helger.css.decl.visit;
+import com.helger.css.decl.CSSNestedDeclarations;
import org.jspecify.annotations.NonNull;
import com.helger.css.decl.CSSDeclaration;
@@ -262,6 +263,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..a36c73fa 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
@@ -232,7 +232,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 +777,91 @@ private void _readStyleDeclarationList (@NonNull final CSSNode aNode,
}
}
+ private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aNode,
+ @NonNull final Consumer 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 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..0168e4e2 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,33 @@ public class CSSWriterSettings implements ICSSWriterSettings, ICloneable 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 +265,24 @@ public final CSSWriterSettings setWriteKeyframesRules (final boolean bWriteKeyfr
return this;
}
+ 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;
@@ -320,8 +364,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 +384,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 +405,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..35a049da 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: "#" )*
@@ -1129,6 +1140,7 @@ void simpleSelectorSequence() #void : {}
( idSelector()
| classSelector()
| attributeSelector()
+ | nestingSelector()
| pseudoClassSelector()
| funcNot()
)*
@@ -1136,6 +1148,7 @@ void simpleSelectorSequence() #void : {}
| ( idSelector()
| classSelector()
| attributeSelector()
+ | nestingSelector()
| pseudoClassSelector()
| funcNot()
)+
@@ -1230,6 +1243,27 @@ void styleDeclarationOrRule() #void : {}
)
}
+void styleDeclarationOrRuleWithNested() #void : {}
+{
+ ( LOOKAHEAD( property() )* ()* | ()* )* )
+ | )* // 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!"); }
+ | 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() { }
+ )
+ ( | )*
+ ( styleDeclarationOrRuleWithNested() )*
+} catch (/*final*/ ParseException ex) {
+ if (m_bBrowserCompliantMode)
+ browserCompliantSkipDecl (ex);
+ else {
+ errorSkipTo (ex, RBRACE);
+ token_source.backup(1);
+ }
+}
+ { return jjtThis; }
+}
+
void styleDeclarationBlock() #void : {}
{
)*
( )*
- selector()
+ selectorOrRelativeSelector()
( )*
)*
- styleDeclarationBlock()
+ styleDeclarationBlockWithNested()
} catch (/*final*/ ParseException ex) {
if (m_bBrowserCompliantMode)
browserCompliantSkipInSelector (ex);
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..f829d83f
--- /dev/null
+++ b/ph-css/src/test/java/com/helger/css/decl/CSSLayerRuleTest.java
@@ -0,0 +1,69 @@
+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 ("""
+ @layer state {
+ .foo {
+ color: white;
+ .bar {
+ color: orange
+ }
+ color: black;
+ }
+ }
+ """);
+ assertEquals (1, aSR.getSelectorCount ());
+ assertEquals (1, aSR.getRuleCount ());
+
+ assertEquals ("state", aSR.getSelectorAtIndex (0));
+
+ CSSStyleRule rule1 = (CSSStyleRule) aSR.getRuleAtIndex (0);
+ assertEquals (1, rule1.getSelectorCount ());
+ assertEquals (1, rule1.getDeclarationCount ());
+ assertEquals (2, rule1.getRuleCount ());
+
+ assertEquals (".foo", rule1.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:white", rule1.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSStyleRule rule11 = (CSSStyleRule) rule1.getRuleAtIndex (0);
+ assertEquals (1, rule11.getSelectorCount ());
+ assertEquals (1, rule11.getDeclarationCount ());
+ assertEquals (0, rule11.getRuleCount ());
+ assertEquals (".bar", rule11.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:orange", rule11.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule12 = (CSSNestedDeclarations) rule1.getRuleAtIndex (1);
+ assertEquals (1, rule12.getDeclarationCount ());
+ assertEquals ("color:black", rule12.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..dea07b61
--- /dev/null
+++ b/ph-css/src/test/java/com/helger/css/decl/CSSMediaRuleTest.java
@@ -0,0 +1,74 @@
+package com.helger.css.decl;
+
+import com.helger.css.reader.CSSReader;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+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 ("""
+ @media print {
+ .foo {
+ color: white;
+ .bar {
+ color: orange
+ }
+ color: black;
+ }
+ }
+ """);
+ assertEquals (1, aSR.getMediaQueryCount ());
+ assertEquals (1, aSR.getRuleCount ());
+
+ CSSMediaQuery mediaQuery = aSR.getMediaQueryAtIndex (0);
+ assertEquals ("print", mediaQuery.getAsCSSString ());
+
+ CSSStyleRule rule1 = (CSSStyleRule) aSR.getRuleAtIndex (0);
+ assertEquals (1, rule1.getSelectorCount ());
+ assertEquals (1, rule1.getDeclarationCount ());
+ assertEquals (2, rule1.getRuleCount ());
+
+ CSSSelector selector1 = rule1.getSelectorAtIndex (0);
+ assertEquals (".foo", selector1.getAsCSSString ());
+
+ CSSDeclaration declaration11 = rule1.getDeclarationAtIndex (0);
+ assertEquals ("color:white", declaration11.getAsCSSString ());
+
+ CSSStyleRule rule11 = (CSSStyleRule) rule1.getRuleAtIndex (0);
+ assertEquals (1, rule11.getSelectorCount ());
+ assertEquals (1, rule11.getDeclarationCount ());
+ assertEquals (0, rule11.getRuleCount ());
+ assertEquals (".bar", rule11.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:orange", rule11.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule12 = (CSSNestedDeclarations) rule1.getRuleAtIndex (1);
+ assertEquals (1, rule12.getDeclarationCount ());
+ assertEquals ("color:black", rule12.getDeclarationAtIndex (0).getAsCSSString ());
+ }
+}
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..90d2c658 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 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 }");
+ CSSSelector aSel = _parse ("div { color:red }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
assertEquals (1, aSel.getMemberCount ());
assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember);
assertEquals ("div", aSel.getMemberAtIndex (0).getAsCSSString ());
+ }
- aSel = _parse ("#id { color:red }");
+ @Test
+ public void testReadIdSelector ()
+ {
+ CSSSelector aSel = _parse ("#id { color:red }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
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 }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
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 }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
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 }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
+ 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 }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
+ 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 }");
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
+ 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 } }");
+
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
+
+ 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 } }");
+
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
+
+ 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 } }");
+
+ assertTrue (m_aIEH.getErrors ().isEmpty ());
+
+ 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..a1eab503 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,184 @@ 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 ());
+
+ CSSStyleRule rule1 = (CSSStyleRule) aSR.getRuleAtIndex (0);
+ assertEquals (1, rule1.getSelectorCount ());
+ assertEquals (1, rule1.getDeclarationCount ());
+ assertEquals (0, rule1.getRuleCount ());
+ assertEquals ("p", rule1.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:dummy", rule1.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSStyleRule rule2 = (CSSStyleRule) aSR.getRuleAtIndex (1);
+ assertEquals (1, rule2.getSelectorCount ());
+ assertEquals (1, rule2.getDeclarationCount ());
+ assertEquals (2, rule2.getRuleCount ());
+ assertEquals (".foobar", rule2.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:green", rule2.getDeclarationAtIndex (0).getAsCSSString ());
+ CSSStyleRule rule21 = (CSSStyleRule) rule2.getRuleAtIndex (0);
+ assertEquals (1, rule21.getDeclarationCount ());
+ assertEquals (0, rule21.getRuleCount ());
+ assertEquals ("color:blue", rule21.getDeclarationAtIndex (0).getAsCSSString ());
+ CSSNestedDeclarations rule22 = (CSSNestedDeclarations) rule2.getRuleAtIndex (1);
+ assertEquals (1, rule22.getDeclarationCount ());
+ assertEquals ("color:white", rule22.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule3 = (CSSNestedDeclarations) aSR.getRuleAtIndex (2);
+ assertEquals (1, rule3.getDeclarationCount ());
+ assertEquals ("color:yellow", rule3.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSMediaRule rule4 = (CSSMediaRule) aSR.getRuleAtIndex (3);
+ assertEquals (1, rule4.getRuleCount ());
+ CSSStyleRule rule41 = (CSSStyleRule)rule4.getRuleAtIndex (0);
+ assertEquals (1, rule41.getSelectorCount ());
+ assertEquals (1, rule41.getDeclarationCount ());
+ assertEquals (1, rule41.getRuleCount ());
+ assertEquals (".print", rule41.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:black", rule41.getDeclarationAtIndex (0).getAsCSSString ());
+ CSSStyleRule rule411 = (CSSStyleRule)rule41.getRuleAtIndex (0);
+ assertEquals (1, rule411.getSelectorCount ());
+ assertEquals (2, rule411.getDeclarationCount ());
+ assertEquals (0, rule411.getRuleCount ());
+ assertEquals ("&:hover", rule411.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:orange", rule411.getDeclarationAtIndex (0).getAsCSSString ());
+ assertEquals ("font-size:20px", rule411.getDeclarationAtIndex (1).getAsCSSString ());
+
+ CSSLayerRule rule5 = (CSSLayerRule) aSR.getRuleAtIndex (4);
+ assertEquals (1, rule5.getSelectorCount ());
+ assertEquals (1, rule5.getRuleCount ());
+ assertEquals ("state", rule5.getSelectorAtIndex (0));
+ assertTrue (rule5.getRuleAtIndex (0) instanceof CSSStyleRule);
+ CSSStyleRule rule51 = (CSSStyleRule)rule5.getRuleAtIndex (0);
+ assertEquals (1, rule51.getSelectorCount ());
+ assertEquals (1, rule51.getDeclarationCount ());
+ assertEquals (1, rule51.getRuleCount ());
+ assertEquals (".alert", rule51.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("background-color:brown", rule51.getDeclarationAtIndex (0).getAsCSSString ());
+ assertEquals (1, rule51.getRuleCount ());
+ CSSStyleRule rule511 = (CSSStyleRule) rule51.getRuleAtIndex (0);
+ assertEquals (1, rule511.getSelectorCount ());
+ assertEquals (1, rule511.getDeclarationCount ());
+ assertEquals (0, rule511.getRuleCount ());
+ assertEquals ("p", rule511.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("border:medium solid limegreen", rule511.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 ());
+
+ CSSStyleRule rule1 = (CSSStyleRule) aSR.getRuleAtIndex (0);
+ assertEquals (1, rule1.getSelectorCount ());
+ assertEquals (1, rule1.getDeclarationCount ());
+ assertEquals (0, rule1.getRuleCount ());
+ assertEquals (".a1", rule1.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:green", rule1.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule2 = (CSSNestedDeclarations) aSR.getRuleAtIndex (1);
+ assertEquals (1, rule2.getDeclarationCount ());
+ assertEquals ("color:blue", rule2.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSStyleRule rule3 = (CSSStyleRule) aSR.getRuleAtIndex (2);
+ assertEquals (1, rule3.getSelectorCount ());
+ assertEquals (1, rule3.getDeclarationCount ());
+ assertEquals (0, rule3.getRuleCount ());
+ assertEquals (".a2", rule3.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:orange", rule3.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule4 = (CSSNestedDeclarations) aSR.getRuleAtIndex (3);
+ assertEquals (1, rule4.getDeclarationCount ());
+ assertEquals ("color:yellow", rule4.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSStyleRule rule5 = (CSSStyleRule) aSR.getRuleAtIndex (4);
+ assertEquals (1, rule5.getSelectorCount ());
+ assertEquals (1, rule5.getDeclarationCount ());
+ assertEquals (0, rule5.getRuleCount ());
+ assertEquals (".a3", rule5.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:white", rule5.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule6 = (CSSNestedDeclarations) aSR.getRuleAtIndex (5);
+ assertEquals (1, rule6.getDeclarationCount ());
+ assertEquals ("color:cyan", rule6.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..b9e168ca 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,44 @@ public void testRead2 ()
.addDeclaration ("color", CSSExpression.createSimple ("red"), false));
TestHelper.testDefaultImplementationWithEqualContentObject (aSR, aCreated);
}
+
+ @Test
+ public void testRead3 ()
+ {
+ CSSSupportsRule aSR = _parse ("""
+ @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 ());
+
+ CSSStyleRule rule1 = (CSSStyleRule) aSR.getRuleAtIndex (0);
+ assertEquals (1, rule1.getSelectorCount ());
+ assertEquals (1, rule1.getDeclarationCount ());
+ assertEquals (2, rule1.getRuleCount ());
+
+ assertEquals (".foo", rule1.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals (".foo", rule1.getSelectorAtIndex (0).getAsCSSString ());
+
+ CSSStyleRule rule11 = (CSSStyleRule) rule1.getRuleAtIndex (0);
+ assertEquals (1, rule11.getSelectorCount ());
+ assertEquals (1, rule11.getDeclarationCount ());
+ assertEquals (0, rule11.getRuleCount ());
+ assertEquals (".bar", rule11.getSelectorAtIndex (0).getAsCSSString ());
+ assertEquals ("color:orange", rule11.getDeclarationAtIndex (0).getAsCSSString ());
+
+ CSSNestedDeclarations rule12 = (CSSNestedDeclarations) rule1.getRuleAtIndex (1);
+ assertEquals (1, rule12.getDeclarationCount ());
+ assertEquals ("color:black", rule12.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..977ccd3d 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,25 @@ 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());
+ }
}
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..c60675a8
--- /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