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:
+ *
+ * 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..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 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 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 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..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.
+ *
+ *
- *
+}
* @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 @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/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 @page rule.
+ * @page {
size: auto;
margin: 10%;
-}@viewport rule.
+ *
+ * @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 Listtrue 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).
+ *
+ * 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)
{
@@ -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}
+ *
+ *
+}
*
* @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..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
+ *
+ *
+ *
+ *
+ * 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;
/**
- *
- *
- * 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.
- * @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 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..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 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: "#" )*
@@ -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() )* ()* | ()* )* )
+ | )* // 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() { }
+ )
+ ( | )*
+ ( 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);
@@ -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() : {}
+{
+ )*
+ )*
+ 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: "