From a929e7af12b4e447d22db1ecb9493d20e22fa3d2 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Mon, 23 Mar 2026 09:10:21 +0100 Subject: [PATCH 01/12] WIP Get started with supporting nested rules phax/ph-css#94 Implements: * Nested style rules (https://drafts.csswg.org/css-nesting-1/#syntax) * The nesting selector (https://drafts.csswg.org/css-nesting-1/#nest-selector) TODO / decisions: * Add support for nested group rules (https://drafts.csswg.org/css-nesting-1/#conditionals) * How should the domain model be extended? * (a) Do we want to preserve the order of interspersed declarations and nested rules? * (b) Does it need to be API compatible? Or can there be minor changes? * (c) Do we want to preserve the distinction between style rule's "style" and "cssRules" attribute from the CSSOM (https://drafts.csswg.org/css-nesting-1/#nested-declarations)? * The easiest way would be to add a list of style rules, a list of media rules etc. to the "CSSStyleRule" class, which fulfills (b) but not (a) and (c). * A couple of test failures related to parsing errors. Since rules can now be nested, edge cases how invalid CSS is handled differ slightly. * Make sure nested rules appear in the output of the getCSSAsString method --- .gitignore | 1 + .../com/helger/css/decl/CSSDeclaration.java | 8 +-- .../css/decl/CSSSelectorSimpleMember.java | 12 +++- .../com/helger/css/decl/CSSStyleRule.java | 72 +++++++++++++++++++ .../css/handler/CSSNodeToDomainObject.java | 26 ++++++- .../com/helger/css/handler/ECSSNodeType.java | 1 + ph-css/src/main/jjtree/ParserCSS30.jjt | 20 ++++-- .../com/helger/css/decl/CSSStyleRuleTest.java | 7 ++ 8 files changed, 133 insertions(+), 14 deletions(-) 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/decl/CSSDeclaration.java b/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java index 13e3ee3f..09dd3120 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSDeclaration.java @@ -35,7 +35,7 @@ import com.helger.css.property.ECSSProperty; /** - * Represents a single element in a CSS style rule. (eg. color:red; + * Represents a single element in a CSS style rule. (e.g. color:red; * or background:uri(a.gif) !important;)
* Instances of this class are mutable since 3.7.4. * @@ -99,7 +99,7 @@ public final String getProperty () @NonNull private static String _unifyProperty (@NonNull final String sProperty) { - // CSS variables are case sensitive (see issue 63) + // CSS variables are case-sensitive (see issue 63) if (sProperty.startsWith ("--")) return sProperty; return sProperty.toLowerCase (Locale.ROOT); @@ -107,7 +107,7 @@ private static String _unifyProperty (@NonNull final String sProperty) /** * Check if this declaration has the specified property. The comparison is - * case insensitive! + * case-insensitive! * * @param sProperty * The property to check. May not be null. @@ -123,7 +123,7 @@ public final boolean hasProperty (@NonNull final String sProperty) /** * Check if this declaration has the specified property. The comparison is - * case insensitive! + * case-insensitive! * * @param eProperty * The property to check. May not be null. diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java index 0fdcc4f4..57ee4bbc 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java @@ -55,11 +55,11 @@ public String getValue () } /** - * @return true if it is no hash, no class and no pseudo selector + * @return true if it is no hash, no class, no pseudo, and no nesting selector */ public boolean isElementName () { - return !isHash () && !isClass () && !isPseudo (); + return !isHash () && !isClass () && !isPseudo () && !isNesting(); } /** @@ -86,6 +86,14 @@ public boolean isPseudo () return m_sValue.charAt (0) == ':'; } + /** + * @return true if it is a nesting selector + */ + 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..4c2cdfe9 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 @@ -47,6 +47,7 @@ public class CSSStyleRule implements ICSSTopLevelRule, IHasCSSDeclarations m_aSelectors = new CommonsArrayList <> (); private final CSSDeclarationContainer m_aDeclarations = new CSSDeclarationContainer (); + private final ICommonsList m_aRules = new CommonsArrayList <> (); private CSSSourceLocation m_aSourceLocation; public CSSStyleRule () @@ -139,6 +140,77 @@ public ICommonsList getAllSelectors () return m_aSelectors.getClone (); } + public boolean hasRules () + { + return m_aRules.isNotEmpty (); + } + + @Nonnegative + public int getRuleCount () + { + return m_aRules.size (); + } + + @NonNull + public CSSStyleRule addRule (@NonNull final CSSStyleRule aRule) + { + ValueEnforcer.notNull (aRule, "Rule"); + + m_aRules.add (aRule); + return this; + } + + @NonNull + public CSSStyleRule addRule (@Nonnegative final int nIndex, @NonNull final CSSStyleRule aRule) + { + ValueEnforcer.isGE0 (nIndex, "Index"); + ValueEnforcer.notNull (aRule, "Rule"); + + if (nIndex >= getRuleCount ()) + m_aRules.add (aRule); + else + m_aRules.add (nIndex, aRule); + return this; + } + + @NonNull + public EChange removeRule (@NonNull final CSSStyleRule aRule) + { + return m_aRules.removeObject (aRule); + } + + @NonNull + public EChange removeRule (@Nonnegative final int nRuleIndex) + { + return m_aRules.removeAtIndex (nRuleIndex); + } + + /** + * Remove all rules. + * + * @return {@link EChange#CHANGED} if any rule was removed, + * {@link EChange#UNCHANGED} otherwise. Never null. + * @since 3.7.3 + */ + @NonNull + public EChange removeAllRules () + { + return m_aRules.removeAll (); + } + + @Nullable + public CSSStyleRule getRuleAtIndex (@Nonnegative final int nRuleIndex) + { + return m_aRules.getAtIndex (nRuleIndex); + } + + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllRules () + { + return m_aRules.getClone (); + } + @NonNull public CSSStyleRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) { 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..2217ce64 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,6 +777,26 @@ private void _readStyleDeclarationList (@NonNull final CSSNode aNode, } } + private void _readStyleDeclarationListRules (@NonNull final CSSNode aNode, + @NonNull final Consumer aConsumer) + { + _expectNodeType (aNode, ECSSNodeType.STYLEDECLARATIONLIST); + // Read all contained declarations + final int nDecls = aNode.jjtGetNumChildren (); + for (int nDecl = 0; nDecl < nDecls; ++nDecl) + { + final CSSNode aChildNode = aNode.jjtGetChild (nDecl); + if (ECSSNodeType.STYLERULE.isNode (aChildNode)) + { + final CSSStyleRule aRule = _createStyleRule (aChildNode); + if (aRule != null) + aConsumer.accept (aRule); + } + // else + // ignore ERROR_SKIP to and all "@" things + } + } + @Nullable private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) { @@ -801,6 +822,9 @@ private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) { // Read all contained declarations _readStyleDeclarationList (aChildNode, ret::addDeclaration); + + // Read all contained rules + _readStyleDeclarationListRules (aChildNode, ret::addRule); } else if (!ECSSNodeType.isErrorNode (aChildNode)) diff --git a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java index 40b7591d..a2ff9254 100644 --- a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java +++ b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java @@ -56,6 +56,7 @@ public enum ECSSNodeType ELEMENTNAME (ParserCSS30TreeConstants.JJTELEMENTNAME), HASH (ParserCSS30TreeConstants.JJTIDSELECTOR), CLASS (ParserCSS30TreeConstants.JJTCLASS), + NESTING (ParserCSS30TreeConstants.JJTNESTING), PSEUDO (ParserCSS30TreeConstants.JJTPSEUDOCLASSSELECTOR), HOST (ParserCSS30TreeConstants.JJTHOST), HOSTCONTEXT (ParserCSS30TreeConstants.JJTHOSTCONTEXT), diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index a15a6714..b9a23d46 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -202,6 +202,7 @@ TOKEN : | < GREATER: ">" > | < TILDE: "~" > | < DOLLAR: "$" > +| < AMPERSAND: "&" > | < HASH: "#" > | < INCLUDES: "~=" > | < DASHMATCH: "|=" > @@ -911,6 +912,11 @@ void typeSelector() #void : {} elementName() } +void nestingSelector() #Nesting : {} +{ + { jjtThis.setText ("&"); } +} + void idSelector() : {} { { jjtThis.setText (token.image); } @@ -1129,6 +1135,7 @@ void simpleSelectorSequence() #void : {} ( idSelector() | classSelector() | attributeSelector() + | nestingSelector() | pseudoClassSelector() | funcNot() )* @@ -1136,6 +1143,7 @@ void simpleSelectorSequence() #void : {} | ( idSelector() | classSelector() | attributeSelector() + | nestingSelector() | pseudoClassSelector() | funcNot() )+ @@ -1213,8 +1221,10 @@ try{ void styleDeclarationOrRule() #void : {} { - ( styleDeclaration() - | ( mediaRule() { errorUnexpectedRule ("@media", "media rule in the middle of a rule-set is not allowed!"); } + ( styleDeclaration() ( LOOKAHEAD( ()* ( | ) ) ()* | ()* ()* ) + | ()* // final semicolon from single line comment + | ( styleRule() + | mediaRule() { errorUnexpectedRule ("@media", "media rule in the middle of a rule-set is not allowed!"); } | 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!"); } @@ -1234,11 +1244,7 @@ CSSNode styleDeclarationList() : {} { try { ( )* - ( styleDeclarationOrRule() )? - ( - ( )* - ( styleDeclarationOrRule() )? - )* + ( styleDeclarationOrRule() )* } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); 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..71cccc6a 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,11 @@ public void testRead2 () ECSSAttributeCase.CASE_INSENSITIVE))); TestHelper.testDefaultImplementationWithEqualContentObject (aSR, aCreated); } + + @Test + public void testRead3 () + { + CSSStyleRule aSR = _parse ("div { color: red; .foobar { color: green; #id { color: red } color: white } }"); + System.out.println(aSR); + } } From 4cb3e704c7665bd0eac095df2d967dccf060e176 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Mon, 23 Mar 2026 09:26:16 +0100 Subject: [PATCH 02/12] Add basic tests for nested rules and nesting selector --- .../com/helger/css/decl/CSSSelectorTest.java | 16 ++++++++++++++++ .../com/helger/css/decl/CSSStyleRuleTest.java | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) 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..06f4c460 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 @@ -82,5 +82,21 @@ public void testRead () assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); assertEquals ("div", aSel.getMemberAtIndex (2).getAsCSSString ()); assertEquals ("#id~div", aSel.getAsCSSString ()); + + aSel = _parse ("&.foo { color:red }"); + 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 ()); + + aSel = _parse (".foo & { color:red }"); + 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 ()); } } 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 71cccc6a..4dd319bd 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 @@ -115,7 +115,21 @@ public void testRead2 () @Test public void testRead3 () { - CSSStyleRule aSR = _parse ("div { color: red; .foobar { color: green; #id { color: red } color: white } }"); - System.out.println(aSR); + CSSStyleRule aSR = _parse ("div { color: red; .foobar { color: green; #id { color: blue } color: white; } }"); + assertEquals (1, aSR.getDeclarationCount ()); + assertEquals (1, aSR.getRuleCount ()); + + assertEquals ("color:red", aSR.getDeclarationAtIndex(0).getAsCSSString()); + + assertEquals (2, aSR.getRuleAtIndex (0).getDeclarationCount()); + assertEquals (1, aSR.getRuleAtIndex (0).getRuleCount()); + + assertEquals ("color:green", aSR.getRuleAtIndex (0).getDeclarationAtIndex(0).getAsCSSString()); + assertEquals ("color:white", aSR.getRuleAtIndex (0).getDeclarationAtIndex(1).getAsCSSString()); + + assertEquals (1, aSR.getRuleAtIndex (0).getRuleAtIndex(0).getDeclarationCount()); + assertEquals (0, aSR.getRuleAtIndex (0).getRuleAtIndex(0).getRuleCount()); + + assertEquals ("color:blue", aSR.getRuleAtIndex (0).getRuleAtIndex(0).getDeclarationAtIndex(0).getAsCSSString()); } } From b9bc6f5a09c1b79e11a6d04439739581bb53dcd9 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 14:59:26 +0100 Subject: [PATCH 03/12] Use CSSNestedDeclarations to represent nested declarations, fix parsing issues, update getAsCssString Add CSSNestedDeclarations class to represent nested declarations, like the CSSOM does. CSSStyleRule now has a list of ICSSNestedRule, which includes various rules such as CSSStyleRule or CSSMediaRule. See https://drafts.csswg.org/css-nesting-1/#conditionals Update CSSNodeToDomainObject to create CSSNestedDeclarations instances where needed. Add support for parsing nested at-rules, e.g.: .foo { @media print {} } Fix parsing issue where the parser confused an element selector with a style declaration, e.g. such as in the following 2 cases: * .foo { p { color: red } } * .foo { p: value; } Add a lookahead that checks if a style declaration follows, and only consume it in that case. Otherwise, proceed to look for rules and nested declarations. Add a new parser rule for consuming a style declaration with nested elements. Not all rules support nesting, e.g. @font-face. For these rules, we do want to keep the previous behavior where it throws on nested rules. Minor change to how whitespace is handled: getAsCssString only outputs itself as a string, it does not output any leading or trailing whitespace or newlines. Space and lines around an CSS element should be handled by the container that contains the element, as it may depend on the context of that container. --- .../java/com/helger/css/ICSSWriteable.java | 9 + .../com/helger/css/ICSSWriterSettings.java | 37 +- .../css/decl/AbstractHasTopLevelRules.java | 52 +++ .../css/decl/CSSDeclarationContainer.java | 15 +- .../com/helger/css/decl/CSSFontFaceRule.java | 22 +- .../com/helger/css/decl/CSSImportRule.java | 2 +- .../com/helger/css/decl/CSSKeyframesRule.java | 31 +- .../com/helger/css/decl/CSSLayerRule.java | 127 +++++- .../com/helger/css/decl/CSSMediaRule.java | 20 +- .../com/helger/css/decl/CSSNamespaceRule.java | 2 +- .../css/decl/CSSNestedDeclarations.java | 215 +++++++++ .../java/com/helger/css/decl/CSSPageRule.java | 61 ++- .../css/decl/CSSSelectorSimpleMember.java | 2 + .../com/helger/css/decl/CSSStyleRule.java | 173 ++++++-- .../com/helger/css/decl/CSSSupportsRule.java | 18 +- .../com/helger/css/decl/CSSUnknownRule.java | 10 +- .../com/helger/css/decl/CSSViewportRule.java | 16 +- .../com/helger/css/decl/CSSWritableList.java | 8 +- .../com/helger/css/decl/ICSSNestedRule.java | 49 +++ .../com/helger/css/decl/ICSSTopLevelRule.java | 29 +- .../helger/css/decl/IHasCSSNestedRules.java | 90 ++++ .../css/handler/CSSNodeToDomainObject.java | 91 +++- .../com/helger/css/handler/ECSSNodeType.java | 1 + .../java/com/helger/css/writer/CSSWriter.java | 64 ++- .../helger/css/writer/CSSWriterSettings.java | 80 +++- ph-css/src/main/jjtree/ParserCSS30.jjt | 65 ++- .../helger/css/decl/CSSImportRuleTest.java | 4 +- .../com/helger/css/decl/CSSLayerRuleTest.java | 68 +++ .../com/helger/css/decl/CSSMediaRuleTest.java | 68 +++ .../com/helger/css/decl/CSSStyleRuleTest.java | 175 +++++++- .../helger/css/decl/CSSSupportsRuleTest.java | 41 ++ .../css/writer/CSSWriterSettingsTest.java | 30 +- .../com/helger/css/writer/CSSWriterTest.java | 416 ++++++++++++++++++ 33 files changed, 1882 insertions(+), 209 deletions(-) create mode 100644 ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java create mode 100644 ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java create mode 100644 ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java create mode 100644 ph-css/src/test/java/com/helger/css/decl/CSSLayerRuleTest.java create mode 100644 ph-css/src/test/java/com/helger/css/decl/CSSMediaRuleTest.java diff --git a/ph-css/src/main/java/com/helger/css/ICSSWriteable.java b/ph-css/src/main/java/com/helger/css/ICSSWriteable.java index d50bf51e..fac59ec6 100644 --- a/ph-css/src/main/java/com/helger/css/ICSSWriteable.java +++ b/ph-css/src/main/java/com/helger/css/ICSSWriteable.java @@ -32,6 +32,9 @@ public interface ICSSWriteable * Get the contents of this object as a serialized CSS string for writing to * an output using the default writer settings. * + *

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

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

The general contract is that this method writes only the content of this object, but not any surrounding context. + * In particular, this method does not add any leading or trailing space or newlines. + * * @param aSettings * The settings to be used to format the output. May not be * null. diff --git a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java index 88e46c41..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 getAllRules (@NonNull final Predicate true if at least one layer rule is contained, false + * otherwise. + * @since 8.2.0 + */ + public boolean hasLayerRules () + { + return m_aRules.containsAny (CSSLayerRule.class::isInstance); + } + + /** + * Get the number of top-level rules that are layer rules (implementing {@link CSSLayerRule}). + * + * @return The number of contained layer rules. Always ≥ 0. + * @since 8.2.0 + */ + @Nonnegative + public int getLayerRuleCount () + { + return m_aRules.getCount (CSSLayerRule.class::isInstance); + } + + /** + * Get the layer rule at the specified index. + * + * @param nIndex + * The index to be resolved. Should be ≥ 0 and < {@link #getStyleRuleCount()}. + * @return The layer rule at the given index, or null if an invalid index was specified. + * @since 8.2.0 + */ + @Nullable + public CSSLayerRule getLayerRuleAtIndex (@Nonnegative final int nIndex) + { + return m_aRules.getAtIndexMapped (CSSLayerRule.class::isInstance, nIndex, CSSLayerRule.class::cast); + } + + /** + * Get a list of all top-level rules that are layer rules (implementing {@link CSSLayerRule}). + * + * @return A copy of all contained layer rules. Never null. + * @since 8.2.0 + */ + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllLayerRules () + { + return m_aRules.getAllMapped (CSSLayerRule.class::isInstance, CSSLayerRule.class::cast); + } + /** * Check if at least one of the top-level rules is a style rule (implementing * {@link CSSStyleRule}). 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/CSSFontFaceRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java index 10b428b0..019cb72e 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSFontFaceRule.java @@ -34,13 +34,15 @@ import com.helger.css.ICSSWriterSettings; /** - * Represents a single @font-face rule.
- * Example:
- * @font-face { - font-family: 'icons'; - src: url(path/to/font.woff) format('woff'); - unicode-range: U+E000-E005; -} + * Represents a single @font-face rule. + * + *

Example: + * + *

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

Example: + * + *

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

Example: + * + *

@media print {
   div#footer {
     display: none;
   }
-}
+}
* * @author Philip Helger */ @NotThreadSafe -public class CSSMediaRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSSourceLocationAware +public class CSSMediaRule extends AbstractHasTopLevelRules implements ICSSTopLevelRule, ICSSNestedRule, ICSSSourceLocationAware { private final ICommonsList m_aMediaQueries = new CommonsArrayList <> (); private CSSSourceLocation m_aSourceLocation; @@ -201,7 +203,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (nRuleCount == 0) { - aSB.append (bOptimizedOutput ? "{}" : " {}" + aSettings.getNewLineString ()); + aSB.append (bOptimizedOutput ? "{}" : " {}"); } else { @@ -217,7 +219,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn bFirst = false; else if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getNewLineString ()); if (!bOptimizedOutput) aSB.append (aSettings.getIndent (nIndentLevel + 1)); @@ -225,10 +227,8 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn } } if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append(aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); } return aSB.toString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java index ac4ac99d..4cb44af5 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSNamespaceRule.java @@ -120,7 +120,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn aSB.append (CSSURLHelper.getAsCSSURL (m_sURL, false)); else aSB.append ("\"\""); - return aSB.append (';').append (aSettings.getNewLineString ()).toString (); + return aSB.append (';').toString (); } @Nullable diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java b/ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java new file mode 100644 index 00000000..adfb6cc8 --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/CSSNestedDeclarations.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2014-2026 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.helger.css.decl; + +import com.helger.annotation.Nonnegative; +import com.helger.annotation.concurrent.NotThreadSafe; +import com.helger.annotation.style.ReturnsMutableCopy; +import com.helger.base.hashcode.HashCodeGenerator; +import com.helger.base.state.EChange; +import com.helger.base.tostring.ToStringGenerator; +import com.helger.collection.commons.ICommonsList; +import com.helger.css.CSSSourceLocation; +import com.helger.css.ICSSSourceLocationAware; +import com.helger.css.ICSSWriterSettings; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Represents nested style declarations. When nesting rules, all CSS style declarations after nested rules are wrapped + * within a nested declarations block, in accordance with the CSS Nesting Module Level 1 specification. A nested + * declarations instance consists of a number of declarations (the styles to be applied to the selected elements). + * + *

Example: + * + *

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

Example: + * + *

@page {
   size: auto;
   margin: 10%;
-}
+}
* * @author Philip Helger */ @@ -170,23 +172,23 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn { // A single declaration aSB.append (bOptimizedOutput ? "{" : " { "); - aSB.append (m_aMembers.getAsCSSString (aSettings, nIndentLevel)); + aSB.append (getPageRuleMemberAsCSS(aSettings, nIndentLevel + 1)); aSB.append (bOptimizedOutput ? "}" : " }"); } else { // More than one declaration aSB.append (bOptimizedOutput ? "{" : " {" + aSettings.getNewLineString ()); - aSB.append (m_aMembers.getAsCSSString (aSettings, nIndentLevel)); + if (!bOptimizedOutput) { + aSB.append (aSettings.getIndent(nIndentLevel + 1)); + } + aSB.append (getPageRuleMemberAsCSS(aSettings, nIndentLevel + 1)); if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel)); + aSB.append (aSettings.getNewLineString ()).append (aSettings.getIndent (nIndentLevel)); aSB.append ('}'); } } - if (!bOptimizedOutput) - aSB.append (aSettings.getNewLineString ()); - return aSB.toString (); } @@ -225,4 +227,43 @@ public String toString () .appendIfNotNull ("SourceLocation", m_aSourceLocation) .getToString (); } + + private String getPageRuleMemberAsCSS(@NonNull ICSSWriterSettings aSettings, int nIndentLevel) { + final boolean bOptimizedOutput = aSettings.isOptimizedOutput (); + + final int nDeclCount = m_aMembers.size (); + if (nDeclCount == 0) + return ""; + if (nDeclCount == 1) + { + // A single element + final StringBuilder aSB = new StringBuilder (); + aSB.append (m_aMembers.get (0).getAsCSSString (aSettings, nIndentLevel)); + // No ';' at the last entry + if (m_aMembers.get (0) instanceof CSSDeclaration) + if (!bOptimizedOutput) + aSB.append (CCSS.DEFINITION_END); + return aSB.toString (); + } + + // More than one element + final StringBuilder aSB = new StringBuilder (); + int nIndex = 0; + for (final ICSSPageRuleMember aElement : m_aMembers) + { + // Indentation + if (!bOptimizedOutput && nIndex != 0) + aSB.append (aSettings.getIndent (nIndentLevel)); + // Emit the main element plus the semicolon + aSB.append (aElement.getAsCSSString (aSettings, nIndentLevel )); + // No ';' at the last decl + if (aElement instanceof CSSDeclaration) + if (!bOptimizedOutput || nIndex < nDeclCount - 1) + aSB.append (CCSS.DEFINITION_END); + if (!bOptimizedOutput && nIndex != m_aMembers.size() -1) + aSB.append (aSettings.getNewLineString ()); + ++nIndex; + } + return aSB.toString (); + } } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java b/ph-css/src/main/java/com/helger/css/decl/CSSSelectorSimpleMember.java index 57ee4bbc..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 @@ -87,7 +87,9 @@ public boolean isPseudo () } /** + * Checks if this selector represents the nesting selector &. * @return true if it is a nesting selector + * @since 8.2.0 */ public boolean isNesting () { 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 4c2cdfe9..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,20 +35,29 @@ /** * Represents a single CSS style rule. A style rule consists of a number of - * selectors (determine the element to which the style rule applies) and a - * number of declarations (the rules to be applied to the selected elements). - *
- * Example:
- * div { color: red; } + * {@link CSSSelector selectors} (determines the elements to which + * the style rule applies), a number of {@link CSSDeclarationContainer declarations} + * (the styles to be applied to the selected elements), and a number of + * {@link ICSSNestedRule nested rules} (the rules nested within the style rule, + * e.g. media rules, supports rules, or nested declarations). + * + *

Example: + * + *

div {
+  color: red;
+  &:hover {
+    color: blue;
+  }
+}
* * @author Philip Helger */ @NotThreadSafe -public class CSSStyleRule implements ICSSTopLevelRule, IHasCSSDeclarations , ICSSSourceLocationAware +public class CSSStyleRule implements ICSSTopLevelRule, ICSSNestedRule, IHasCSSDeclarations , IHasCSSNestedRules, ICSSSourceLocationAware { private final ICommonsList m_aSelectors = new CommonsArrayList <> (); private final CSSDeclarationContainer m_aDeclarations = new CSSDeclarationContainer (); - private final ICommonsList m_aRules = new CommonsArrayList <> (); + private final ICommonsList m_aRules = new CommonsArrayList <> (); private CSSSourceLocation m_aSourceLocation; public CSSStyleRule () @@ -140,19 +150,22 @@ public ICommonsList getAllSelectors () return m_aSelectors.getClone (); } - public boolean hasRules () + @Override + public boolean hasRules() { return m_aRules.isNotEmpty (); } + @Override @Nonnegative - public int getRuleCount () + public int getRuleCount() { return m_aRules.size (); } + @Override @NonNull - public CSSStyleRule addRule (@NonNull final CSSStyleRule aRule) + public CSSStyleRule addRule(@NonNull final ICSSNestedRule aRule) { ValueEnforcer.notNull (aRule, "Rule"); @@ -160,8 +173,9 @@ public CSSStyleRule addRule (@NonNull final CSSStyleRule aRule) return this; } + @Override @NonNull - public CSSStyleRule addRule (@Nonnegative final int nIndex, @NonNull final CSSStyleRule aRule) + public CSSStyleRule addRule(@Nonnegative final int nIndex, @NonNull final ICSSNestedRule aRule) { ValueEnforcer.isGE0 (nIndex, "Index"); ValueEnforcer.notNull (aRule, "Rule"); @@ -173,44 +187,43 @@ public CSSStyleRule addRule (@Nonnegative final int nIndex, @NonNull final CSSSt return this; } + @Override @NonNull - public EChange removeRule (@NonNull final CSSStyleRule aRule) + public EChange removeRule(@NonNull final ICSSNestedRule aRule) { return m_aRules.removeObject (aRule); } + @Override @NonNull - public EChange removeRule (@Nonnegative final int nRuleIndex) + public EChange removeRule(@Nonnegative final int nRuleIndex) { return m_aRules.removeAtIndex (nRuleIndex); } - /** - * Remove all rules. - * - * @return {@link EChange#CHANGED} if any rule was removed, - * {@link EChange#UNCHANGED} otherwise. Never null. - * @since 3.7.3 - */ + @Override @NonNull - public EChange removeAllRules () + public EChange removeAllRules() { return m_aRules.removeAll (); } + @Override @Nullable - public CSSStyleRule getRuleAtIndex (@Nonnegative final int nRuleIndex) + public ICSSNestedRule getRuleAtIndex(@Nonnegative final int nRuleIndex) { return m_aRules.getAtIndex (nRuleIndex); } + @Override @NonNull @ReturnsMutableCopy - public ICommonsList getAllRules () + public ICommonsList getAllRules() { return m_aRules.getClone (); } + @Override @NonNull public CSSStyleRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) { @@ -218,6 +231,7 @@ public CSSStyleRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) return this; } + @Override @NonNull public CSSStyleRule addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) { @@ -225,24 +239,28 @@ public CSSStyleRule addDeclaration (@Nonnegative final int nIndex, @NonNull fina return this; } + @Override @NonNull public EChange removeDeclaration (@NonNull final CSSDeclaration aDeclaration) { return m_aDeclarations.removeDeclaration (aDeclaration); } + @Override @NonNull public EChange removeDeclaration (@Nonnegative final int nDeclarationIndex) { return m_aDeclarations.removeDeclaration (nDeclarationIndex); } + @Override @NonNull public EChange removeAllDeclarations () { return m_aDeclarations.removeAllDeclarations (); } + @Override @NonNull @ReturnsMutableCopy public ICommonsList getAllDeclarations () @@ -250,12 +268,14 @@ public ICommonsList getAllDeclarations () return m_aDeclarations.getAllDeclarations (); } + @Override @Nullable public CSSDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) { return m_aDeclarations.getDeclarationAtIndex (nIndex); } + @Override @NonNull public CSSStyleRule setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) { @@ -263,23 +283,27 @@ public CSSStyleRule setDeclarationAtIndex (@Nonnegative final int nIndex, @NonNu return this; } + @Override public boolean hasDeclarations () { return m_aDeclarations.hasDeclarations (); } + @Override @Nonnegative public int getDeclarationCount () { return m_aDeclarations.getDeclarationCount (); } + @Override @Nullable public CSSDeclaration getDeclarationOfPropertyName (@Nullable final String sPropertyName) { return m_aDeclarations.getDeclarationOfPropertyName (sPropertyName); } + @Override @NonNull @ReturnsMutableCopy public ICommonsList getAllDeclarationsOfPropertyName (@Nullable final String sPropertyName) @@ -287,6 +311,16 @@ public ICommonsList getAllDeclarationsOfPropertyName (@Nullable return m_aDeclarations.getAllDeclarationsOfPropertyName (sPropertyName); } + /** + * Get the selectors as a serialized CSS string for writing to an output. + * + * @param aSettings + * The settings to be used to format the output. May not be + * null. + * @param nIndentLevel + * The current indentation level + * @return The content of the selectors as CSS string. Never null. + */ @NonNull public String getSelectorsAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) { @@ -308,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; @@ -347,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 @@ -361,6 +475,7 @@ public String toString () { return new ToStringGenerator (this).append ("selectors", m_aSelectors) .append ("declarations", m_aDeclarations) + .append ("rules", m_aRules) .appendIfNotNull ("SourceLocation", m_aSourceLocation) .getToString (); } diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java index 85ca62c2..5c21b0a4 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSSupportsRule.java @@ -36,16 +36,18 @@ /** * Represents a single @supports rule: a list of style rules only valid when a certain - * declaration is available. See {@link com.helger.css.ECSSSpecification#CSS3_CONDITIONAL}
- * Example:
- * @supports (transition-property: color) { + * declaration is available. See {@link com.helger.css.ECSSSpecification#CSS3_CONDITIONAL} + * + *

Example: + * + *

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

Example: + * + *

@viewport {
+  width: device-width;
+}
* * @author Philip Helger */ @@ -157,11 +161,7 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (aSettings.isRemoveUnnecessaryCode () && !hasDeclarations ()) return ""; - final StringBuilder aSB = new StringBuilder (m_sDeclaration); - aSB.append (m_aDeclarations.getAsCSSString (aSettings, nIndentLevel)); - if (!aSettings.isOptimizedOutput ()) - aSB.append (aSettings.getNewLineString ()); - return aSB.toString (); + return m_sDeclaration + m_aDeclarations.getAsCSSString(aSettings, nIndentLevel); } @Nullable diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java b/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java index 44ffaf8e..cc026a75 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSWritableList.java @@ -74,14 +74,14 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn for (final DATATYPE aElement : this) { // Indentation - if (!bOptimizedOutput) - aSB.append (aSettings.getIndent (nIndentLevel + 1)); + if (!bOptimizedOutput && nIndex != 0) + aSB.append (aSettings.getIndent (nIndentLevel)); // Emit the main element plus the semicolon - aSB.append (aElement.getAsCSSString (aSettings, nIndentLevel + 1)); + aSB.append (aElement.getAsCSSString (aSettings, nIndentLevel )); // No ';' at the last decl if (!bOptimizedOutput || nIndex < nDeclCount - 1) aSB.append (CCSS.DEFINITION_END); - if (!bOptimizedOutput) + if (!bOptimizedOutput && nIndex != size() - 1) aSB.append (aSettings.getNewLineString ()); ++nIndex; } diff --git a/ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java b/ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java new file mode 100644 index 00000000..4beb8f3d --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/ICSSNestedRule.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014-2026 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.helger.css.decl; + +import com.helger.annotation.style.MustImplementEqualsAndHashcode; +import com.helger.css.ICSSWriteable; + +/** + * Marker interface for all nested CSS elements that can occur in any order + * + *
    + *
  • layer rules - {@link CSSLayerRule} + *
  • media rules - {@link CSSMediaRule} + *
  • nested declarations - {@link CSSNestedDeclarations} + *
  • style rules - {@link CSSStyleRule} + *
  • supports rules - {@link CSSSupportsRule} + *
  • unknown rules - {@link CSSUnknownRule} + *
+ * + * To easily iterate over all rules contained in a {@link CascadingStyleSheet} + * you can use the + * {@link com.helger.css.decl.visit.CSSVisitor#visitCSS(CascadingStyleSheet, com.helger.css.decl.visit.ICSSVisitor) CSSVisitor#visitCSS(sheet, visitor)} + * method. An empty stub implementation of + * {@link com.helger.css.decl.visit.ICSSVisitor ICSSVisitor} is the class + * {@link com.helger.css.decl.visit.DefaultCSSVisitor DefaultCSSVisitor} which is a good basis for + * your own implementations. + * + * @author Philip Helger + * @since 8.2.0 + */ +@MustImplementEqualsAndHashcode +public interface ICSSNestedRule extends ICSSWriteable +{ + /* empty */ +} diff --git a/ph-css/src/main/java/com/helger/css/decl/ICSSTopLevelRule.java b/ph-css/src/main/java/com/helger/css/decl/ICSSTopLevelRule.java index 933cc1cc..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; /** - *

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

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

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

* * @author Philip Helger */ diff --git a/ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java b/ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java new file mode 100644 index 00000000..cd93f975 --- /dev/null +++ b/ph-css/src/main/java/com/helger/css/decl/IHasCSSNestedRules.java @@ -0,0 +1,90 @@ +package com.helger.css.decl; + +import com.helger.annotation.Nonnegative; +import com.helger.annotation.style.ReturnsMutableCopy; +import com.helger.base.state.EChange; +import com.helger.collection.commons.ICommonsList; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Sanity interface for all objects having nested CSS rules. + * + * @param + * Implementation type + * @author Philip Helger + * @since 8.2.0 + */ +public interface IHasCSSNestedRules { + /** + * Checks if this element has any nested rules. + * @return true if at least one nested rule is present, false otherwise. Never + * null. + */ + boolean hasRules(); + + /** + * Gets the number of nested rules contained in this element. + * @return The number of nested rules. Never negative. + */ + @Nonnegative + int getRuleCount(); + + /** + * Adds a rule to the list of nested rules. The rule is added at the end of the list. + * @param aRule The rule to be added. Must not be null. + * @return This element for chaining. Never null. + */ + @NonNull IMPLTYPE addRule(@NonNull ICSSNestedRule aRule); + + /** + * Adds a rule to the list of nested rules at the specified index. + * @param nIndex The index at which the rule should be added. If equal to or greater than the current number of rules, + * the rule will be added at the end of the list. Must not be negative. + * @param aRule The rule to be added. Must not be null. + * @return This element for chaining. Never null. + */ + @NonNull IMPLTYPE addRule(@Nonnegative int nIndex, @NonNull ICSSNestedRule aRule); + + /** + * Removes a rule from the list of nested rules, if present. + * @param aRule The rule to be removed. Must not be null. + * @return {@link EChange#CHANGED CHANGED} if the rule was removed, {@link EChange#UNCHANGED UNCHANGED} otherwise. + * Never null. + */ + @NonNull EChange removeRule(@NonNull ICSSNestedRule aRule); + + /** + * Removes a rule from the list of nested rules at the specified index. + * @param nRuleIndex The index of the rule to be removed. Values equal to or greater than the current number of rules + * will be ignored. Must not be negative. + * @return {@link EChange#CHANGED CHANGED} if the rule was removed, {@link EChange#UNCHANGED UNCHANGED} otherwise. + * Never null. + */ + @NonNull EChange removeRule(@Nonnegative int nRuleIndex); + + /** + * Remove all nested rules. + * + * @return {@link EChange#CHANGED CHANGED} if any rule was removed, {@link EChange#UNCHANGED UNCHANGED} otherwise. + * Never null. + */ + @NonNull EChange removeAllRules(); + + /** + * Gets a nested rule at the specified index. + * @param nRuleIndex The index of the rule to be retrieved. If equal to or greater than the current number of rules, + * null will be returned. Must not be negative. + * @return This rule for chaining. null if not found. + */ + @Nullable ICSSNestedRule getRuleAtIndex(@Nonnegative int nRuleIndex); + + /** + * Gets all nested rules contained in this element. Modifications to the returned list will not affect this style + * rule, and vice versa. + * @return A list of nested rules. Never null. + */ + @NonNull + @ReturnsMutableCopy + ICommonsList getAllRules(); +} diff --git a/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java b/ph-css/src/main/java/com/helger/css/handler/CSSNodeToDomainObject.java index 2217ce64..36bd9e59 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 @@ -777,24 +777,86 @@ private void _readStyleDeclarationList (@NonNull final CSSNode aNode, } } - private void _readStyleDeclarationListRules (@NonNull final CSSNode aNode, - @NonNull final Consumer aConsumer) + private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aNode, + @NonNull final Consumer aDeclarationConsumer, + @NonNull final Consumer aNestedRuleConsumer) { - _expectNodeType (aNode, ECSSNodeType.STYLEDECLARATIONLIST); - // Read all contained declarations + _expectNodeType (aNode, ECSSNodeType.STYLEDECLARATIONLISTWITHNESTED); + // Read all contained declarations and rules final int nDecls = aNode.jjtGetNumChildren (); + CSSNestedDeclarations aNestedDeclarations = null; for (int nDecl = 0; nDecl < nDecls; ++nDecl) { final CSSNode aChildNode = aNode.jjtGetChild (nDecl); - if (ECSSNodeType.STYLERULE.isNode (aChildNode)) + if (ECSSNodeType.STYLEDECLARATION.isNode (aChildNode)) { - final CSSStyleRule aRule = _createStyleRule (aChildNode); - if (aRule != null) - aConsumer.accept (aRule); + final CSSDeclaration aDeclaration = _createDeclaration (aChildNode); + if (aDeclaration != null) { + // declarations that appear at the start are added as declarations of the style rule + // declarations that appear interspersed with other rules are wrapped in a nested declarations element + if (aNestedDeclarations != null) { + aNestedDeclarations.addDeclaration(aDeclaration); + } else { + aDeclarationConsumer.accept (aDeclaration); + } + } } - // else - // ignore ERROR_SKIP to and all "@" things + else + if (ECSSNodeType.STYLERULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSStyleRule aRule = _createStyleRule (aChildNode); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSMediaRule aRule = _createMediaRule (aChildNode); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSSupportsRule aRule = _createSupportsRule (aChildNode); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSLayerRule aRule = _createLayerRule (aChildNode); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + else + if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) + { + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); + final CSSUnknownRule aRule = _createUnknownRule (aChildNode); + if (aRule != null) + aNestedRuleConsumer.accept (aRule); + aNestedDeclarations = new CSSNestedDeclarations(); + } + // else + // ignore ERROR_SKIP to and all unsupported nested "@" rules } + // append trailing declarations if there are any + if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) + aNestedRuleConsumer.accept (aNestedDeclarations); } @Nullable @@ -818,13 +880,10 @@ private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) { // OK, we're after the selectors bSelectors = false; - if (ECSSNodeType.STYLEDECLARATIONLIST.isNode (aChildNode)) + if (ECSSNodeType.STYLEDECLARATIONLISTWITHNESTED.isNode (aChildNode)) { - // Read all contained declarations - _readStyleDeclarationList (aChildNode, ret::addDeclaration); - - // Read all contained rules - _readStyleDeclarationListRules (aChildNode, ret::addRule); + // Read all contained declarations and nested rules + _readStyleDeclarationListWithNestedRules (aChildNode, ret::addDeclaration, ret::addRule); } else if (!ECSSNodeType.isErrorNode (aChildNode)) diff --git a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java index a2ff9254..004604fb 100644 --- a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java +++ b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java @@ -50,6 +50,7 @@ public enum ECSSNodeType SELECTOR (ParserCSS30TreeConstants.JJTSELECTOR), RELATIVESELECTOR (ParserCSS30TreeConstants.JJTRELATIVESELECTOR), STYLEDECLARATIONLIST (ParserCSS30TreeConstants.JJTSTYLEDECLARATIONLIST), + STYLEDECLARATIONLISTWITHNESTED (ParserCSS30TreeConstants.JJTSTYLEDECLARATIONLISTWITHNESTED), STYLEDECLARATION (ParserCSS30TreeConstants.JJTSTYLEDECLARATION), // style rule -- selector NAMESPACEPREFIX (ParserCSS30TreeConstants.JJTNAMESPACEPREFIX), diff --git a/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java b/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java index a5dafbab..7c32701f 100644 --- a/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java +++ b/ph-css/src/main/java/com/helger/css/writer/CSSWriter.java @@ -44,7 +44,7 @@ @NotThreadSafe public class CSSWriter { - /** By default optimized output is disabled */ + /** By default, optimized output is disabled */ public static final boolean DEFAULT_OPTIMIZED_OUTPUT = CSSWriterSettings.DEFAULT_OPTIMIZED_OUTPUT; private final CSSWriterSettings m_aSettings; @@ -89,7 +89,7 @@ public CSSWriter (@NonNull final CSSWriterSettings aSettings) } /** - * Check if the header text should be emitted. By default it is enabled, if non-optimized output + * Check if the header text should be emitted. By default, it is enabled, if non-optimized output * is desired. * * @return true if the header text should be emitted, false if not. @@ -100,7 +100,7 @@ public boolean isWriteHeaderText () } /** - * Determine whether the file header should be written or not. By default it is enabled, if + * Determine whether the file header should be written or not. By default, it is enabled, if * non-optimized output is desired. * * @param bWriteHeaderText @@ -126,7 +126,7 @@ public String getHeaderText () /** * Set a custom header text that should be emitted. This text may be multi line separated by the - * '\n' character. It will emitted if {@link #isWriteHeaderText()} returns true. + * '\n' character. It will be emitted if {@link #isWriteHeaderText()} returns true. * * @param sHeaderText * The header text to be emitted. May be null. @@ -140,7 +140,7 @@ public CSSWriter setHeaderText (@Nullable final String sHeaderText) } /** - * Check if the footer text should be emitted. By default it is enabled, if non-optimized output + * Check if the footer text should be emitted. By default, it is enabled, if non-optimized output * is desired. * * @return true if the footer text should be emitted, false if not. @@ -151,7 +151,7 @@ public boolean isWriteFooterText () } /** - * Determine whether the file footer should be written or not. By default it is enabled, if + * Determine whether the file footer should be written or not. By default, it is enabled, if * non-optimized output is desired. * * @param bWriteFooterText @@ -177,7 +177,7 @@ public String getFooterText () /** * Set a custom footer text that should be emitted. This text may be multi line separated by the - * '\n' character. It will emitted if {@link #isWriteFooterText()} returns true. + * '\n' character. It will be emitted if {@link #isWriteFooterText()} returns true. * * @param sFooterText * The footer text to be emitted. May be null. @@ -191,7 +191,7 @@ public CSSWriter setFooterText (@Nullable final String sFooterText) } /** - * @return The current defined content charset for the CSS. By default it is null. + * @return The current defined content charset for the CSS. By default, it is null. */ @Nullable public String getContentCharset () @@ -201,7 +201,7 @@ public String getContentCharset () /** * Define the content charset to be used. If not null and not empty, the - * @charset element is emitted into the CSS. By default no charset is defined.
+ * @charset element is emitted into the CSS. By default, no charset is defined.
* Important: this does not define the encoding of the output - it is just a declarative * marker inside the code. Best practice is to use the same encoding for the CSS and the * respective writer! @@ -235,7 +235,7 @@ public CSSWriterSettings getSettings () * @param aCSS * The CSS to write. May not be null. * @param aWriter - * The write to write the text to. May not be null. Is automatically closed + * The writer to write the text to. May not be null. Is automatically closed * after the writing! * @throws IOException * In case writing fails. @@ -268,32 +268,58 @@ public void writeCSS (@NonNull final CascadingStyleSheet aCSS, @NonNull @WillClo aWriter.write (sNewLineString); } + int nRulesEmitted = 0; + // Charset? Must be the first element before the import if (StringHelper.isNotEmpty (m_sContentCharset)) { aWriter.write ("@charset \"" + m_sContentCharset + "\";"); - if (!bOptimizedOutput) - aWriter.write (sNewLineString); + ++nRulesEmitted; } // Import rules - int nRulesEmitted = 0; + boolean bFirst = true; final ICommonsList aImportRules = aCSS.getAllImportRules (); if (aImportRules.isNotEmpty ()) + { + if (!bOptimizedOutput && nRulesEmitted > 0) + { + aWriter.write(sNewLineString); + aWriter.write(sNewLineString); + } for (final CSSImportRule aImportRule : aImportRules) { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aWriter.write (sNewLineString); aWriter.write (aImportRule.getAsCSSString (m_aSettings)); ++nRulesEmitted; } + } // Namespace rules + bFirst = true; final ICommonsList aNamespaceRules = aCSS.getAllNamespaceRules (); if (aNamespaceRules.isNotEmpty ()) + { + if (!bOptimizedOutput && nRulesEmitted > 0) + { + aWriter.write(sNewLineString); + aWriter.write(sNewLineString); + } for (final CSSNamespaceRule aNamespaceRule : aNamespaceRules) { + if (bFirst) + bFirst = false; + else + if (!bOptimizedOutput) + aWriter.write (sNewLineString); aWriter.write (aNamespaceRule.getAsCSSString (m_aSettings)); ++nRulesEmitted; } + } // Main CSS rules for (final ICSSTopLevelRule aRule : aCSS.getAllRules ()) @@ -302,13 +328,21 @@ public void writeCSS (@NonNull final CascadingStyleSheet aCSS, @NonNull @WillClo if (StringHelper.isNotEmpty (sRuleCSS)) { if (!bOptimizedOutput && nRulesEmitted > 0) - aWriter.write (sNewLineString); + { + aWriter.write(sNewLineString); + aWriter.write(sNewLineString); + } aWriter.write (sRuleCSS); ++nRulesEmitted; } } + // Newline after all rules + if (!bOptimizedOutput && nRulesEmitted > 0){ + aWriter.write (sNewLineString); + } + // Write file footer if (m_bWriteFooterText && StringHelper.isNotEmpty (m_sFooterText)) { @@ -360,7 +394,7 @@ public String getCSSAsString (@NonNull final CascadingStyleSheet aCSS) * @param aCSS * The CSS to write. May not be null. * @param aWriter - * The write to write the text to. May not be null. Is automatically closed + * The writer to write the text to. May not be null. Is automatically closed * after the writing! * @throws IOException * In case writing fails. diff --git a/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/writer/CSSWriterSettings.java index 9997b1b8..dcb0c4bb 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 true to write nested declarations, false to ignore them. + * @return This instance for chaining + * @since 8.2.0 + */ + @NonNull + public final CSSWriterSettings setWriteNestedDeclarations(final boolean bWriteNestedDeclarations) + { + m_bWriteNestedDeclarations = bWriteNestedDeclarations; + return this; + } + public final boolean isWriteFontFaceRules () { return m_bWriteFontFaceRules; @@ -239,6 +266,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; @@ -320,8 +366,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 +386,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 +407,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 b9a23d46..cc53973d 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1221,10 +1221,8 @@ try{ void styleDeclarationOrRule() #void : {} { - ( styleDeclaration() ( LOOKAHEAD( ()* ( | ) ) ()* | ()* ()* ) - | ()* // final semicolon from single line comment - | ( styleRule() - | mediaRule() { errorUnexpectedRule ("@media", "media rule in the middle of a rule-set is not allowed!"); } + ( styleDeclaration() + | ( mediaRule() { errorUnexpectedRule ("@media", "media rule in the middle of a rule-set is not allowed!"); } | 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!"); } @@ -1240,11 +1238,52 @@ void styleDeclarationOrRule() #void : {} ) } +void styleDeclarationOrRuleWithNested() #void : {} +{ + ( LOOKAHEAD( property() ) styleDeclaration() ( LOOKAHEAD( ()* ( | ) ) ()* | ()* ()* ) + | ()* // final semicolon from single line comment + | ( styleRule() + | mediaRule() { } + | pageRule() { errorUnexpectedRule ("@page", "page rule is not allowed as a nested rule!"); } + | fontfaceRule() { errorUnexpectedRule ("@font-face", "font-face rule is not allowed as a nested rule!"); } + | keyframesRule() { errorUnexpectedRule ("@keyframes", "keyframes rule is not allowed as a nested rule!"); } + | viewportRule() { errorUnexpectedRule ("@viewport", "viewport rule is not allowed as a nested rule!"); } + | supportsRule() { } + | unknownRule() { } + | charsetRule() { errorUnexpectedRule ("@charset", "charset rule is not allowed as a nested rule!"); } + | importRule() { errorUnexpectedRule ("@import", "import rule is not allowed as a nested rule!"); } + | namespaceRule() { errorUnexpectedRule ("@namespace", "namespace rule is not allowed as a nested rule!"); } + | layerRule() { } + ) + ( | | )* + ) +} + CSSNode styleDeclarationList() : {} { try { ( )* - ( styleDeclarationOrRule() )* + ( styleDeclarationOrRule() )? + ( + ( )* + ( styleDeclarationOrRule() )? + )* +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else { + errorSkipTo (ex, RBRACE); + token_source.backup(1); + } +} + { return jjtThis; } +} + +CSSNode styleDeclarationListWithNested() : {} +{ +try { + ( )* + ( styleDeclarationOrRuleWithNested() )* } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); @@ -1262,6 +1301,20 @@ void styleDeclarationBlock() #void : {} try { styleDeclarationList() +} catch (/*final*/ ParseException ex) { + if (m_bBrowserCompliantMode) + browserCompliantSkipDecl (ex); + else + errorSkipTo (ex, RBRACE); +} +} + +void styleDeclarationBlockWithNested() #void : {} +{ + +try { + styleDeclarationListWithNested() + } catch (/*final*/ ParseException ex) { if (m_bBrowserCompliantMode) browserCompliantSkipDecl (ex); @@ -1280,7 +1333,7 @@ try{ selector() ( )* )* - 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..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/CSSStyleRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java index 4dd319bd..6d3839f0 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSStyleRuleTest.java @@ -115,21 +115,178 @@ public void testRead2 () @Test public void testRead3 () { - CSSStyleRule aSR = _parse ("div { color: red; .foobar { color: green; #id { color: blue } color: white; } }"); - assertEquals (1, aSR.getDeclarationCount ()); + CSSStyleRule aSR = _parse ("div { p { color: red; } }"); + assertEquals (1, aSR.getSelectorCount ()); + assertEquals (0, aSR.getDeclarationCount ()); assertEquals (1, aSR.getRuleCount ()); + assertEquals("div", aSR.getSelectorAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + assertEquals ("p", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:red", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + } + + @Test + public void testRead4 () + { + CSSStyleRule aSR = _parse (""" + div { + color: red; + p: dummy; + p { + color: dummy; + } + .foobar { + color: green; + #id { + color: blue + } + color: white + } + color: yellow; + @media print { + .print { + color: black; + &:hover { + color: orange; + font-size: 20px; + } + } + } + @layer state { + .alert { + background-color: brown; + p { + border: medium solid limegreen; + } + } + } + }"""); + assertEquals (2, aSR.getDeclarationCount ()); + assertEquals (5, aSR.getRuleCount ()); + + assertEquals ("color:red", aSR.getDeclarationAtIndex(0).getAsCSSString()); + assertEquals ("p:dummy", aSR.getDeclarationAtIndex(1).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + assertEquals ("p", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:dummy", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (1) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (1)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (1)).getDeclarationCount()); + assertEquals (2, ((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleCount()); + assertEquals (".foobar", ((CSSStyleRule)aSR.getRuleAtIndex (1)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:green", ((CSSStyleRule)aSR.getRuleAtIndex (1)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0)).getRuleCount()); + assertEquals ("color:blue", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(1)).getDeclarationCount()); + assertEquals ("color:white", ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (1)).getRuleAtIndex(1)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (2) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (2)).getDeclarationCount()); + assertEquals ("color:yellow", ((CSSNestedDeclarations)aSR.getRuleAtIndex (2)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (3) instanceof CSSMediaRule); + assertEquals (1, ((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleCount()); + assertTrue (((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (1, ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".print", ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:black", ((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (2, ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals ("&:hover", ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertEquals ("font-size:20px", ((CSSStyleRule)((CSSStyleRule)((CSSMediaRule)aSR.getRuleAtIndex (3)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationAtIndex(1).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (4) instanceof CSSLayerRule); + assertEquals (1, ((CSSLayerRule)aSR.getRuleAtIndex (4)).getSelectorCount()); + assertEquals (1, ((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleCount()); + assertEquals ("state", ((CSSLayerRule)aSR.getRuleAtIndex (4)).getSelectorAtIndex(0)); + assertTrue (((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (1, ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".alert", ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("background-color:brown", ((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals ("p", ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("border:medium solid limegreen", ((CSSStyleRule)((CSSStyleRule)((CSSLayerRule)aSR.getRuleAtIndex (4)).getRuleAtIndex(0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + } + + @Test + public void testRead5 () + { + CSSStyleRule aSR = _parse (""" + div { + color: red; + .a1 { color: green; } + color: blue; + .a2 { color: orange; } + color: yellow; + .a3 { color: white; } + color: cyan; + } + """); + assertEquals (1, aSR.getSelectorCount ()); + assertEquals (1, aSR.getDeclarationCount ()); + assertEquals (6, aSR.getRuleCount ()); + + assertEquals ("div", aSR.getSelectorAtIndex(0).getAsCSSString()); assertEquals ("color:red", aSR.getDeclarationAtIndex(0).getAsCSSString()); - assertEquals (2, aSR.getRuleAtIndex (0).getDeclarationCount()); - assertEquals (1, aSR.getRuleAtIndex (0).getRuleCount()); + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + assertEquals (".a1", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:green", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (1)).getDeclarationCount()); + assertEquals ("color:blue", ((CSSNestedDeclarations)aSR.getRuleAtIndex (1)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (2) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (2)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (2)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (2)).getRuleCount()); + assertEquals (".a2", ((CSSStyleRule)aSR.getRuleAtIndex (2)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)aSR.getRuleAtIndex (2)).getDeclarationAtIndex(0).getAsCSSString()); - assertEquals ("color:green", aSR.getRuleAtIndex (0).getDeclarationAtIndex(0).getAsCSSString()); - assertEquals ("color:white", aSR.getRuleAtIndex (0).getDeclarationAtIndex(1).getAsCSSString()); + assertTrue (aSR.getRuleAtIndex (3) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (3)).getDeclarationCount()); + assertEquals ("color:yellow", ((CSSNestedDeclarations)aSR.getRuleAtIndex (3)).getDeclarationAtIndex(0).getAsCSSString()); - assertEquals (1, aSR.getRuleAtIndex (0).getRuleAtIndex(0).getDeclarationCount()); - assertEquals (0, aSR.getRuleAtIndex (0).getRuleAtIndex(0).getRuleCount()); + assertTrue (aSR.getRuleAtIndex (4) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (4)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (4)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)aSR.getRuleAtIndex (4)).getRuleCount()); + assertEquals (".a3", ((CSSStyleRule)aSR.getRuleAtIndex (4)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:white", ((CSSStyleRule)aSR.getRuleAtIndex (4)).getDeclarationAtIndex(0).getAsCSSString()); - assertEquals ("color:blue", aSR.getRuleAtIndex (0).getRuleAtIndex(0).getDeclarationAtIndex(0).getAsCSSString()); + assertTrue (aSR.getRuleAtIndex (5) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)aSR.getRuleAtIndex (5)).getDeclarationCount()); + assertEquals ("color:cyan", ((CSSNestedDeclarations)aSR.getRuleAtIndex (5)).getDeclarationAtIndex(0).getAsCSSString()); } } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java index e8b6e735..b66b510c 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSSupportsRuleTest.java @@ -28,6 +28,8 @@ import com.helger.css.reader.CSSReader; import com.helger.unittest.support.TestHelper; +import java.util.List; + /** * Test class for {@link CSSSupportsRule}. * @@ -128,4 +130,43 @@ public void testRead2 () .addDeclaration ("color", CSSExpression.createSimple ("red"), false)); TestHelper.testDefaultImplementationWithEqualContentObject (aSR, aCreated); } + + @Test + public void testRead3 () + { + CSSSupportsRule aSR = _parse (String.join("\n", List.of( + "@supports(column-count: 1) {", + " .foo {", + " color: white;", + " .bar {", + " color: orange", + " }", + " color: black;", + " }", + "}"))); + + assertEquals (1, aSR.getSupportsConditionMemberCount ()); + assertEquals (1, aSR.getRuleCount ()); + + assertEquals ("(column-count:1)", aSR.getSupportsConditionMemberAtIndex (0).getAsCSSString()); + + assertTrue (aSR.getRuleAtIndex (0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getDeclarationCount()); + assertEquals (2, ((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleCount()); + + assertEquals (".foo", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals (".foo", ((CSSStyleRule)aSR.getRuleAtIndex (0)).getSelectorAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorCount()); + assertEquals (1, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationCount()); + assertEquals (0, ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getRuleCount()); + assertEquals (".bar", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getSelectorAtIndex(0).getAsCSSString()); + assertEquals ("color:orange", ((CSSStyleRule)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(0)).getDeclarationAtIndex(0).getAsCSSString()); + + assertTrue (((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1) instanceof CSSNestedDeclarations); + assertEquals (1, ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationCount()); + assertEquals ("color:black", ((CSSNestedDeclarations)((CSSStyleRule)aSR.getRuleAtIndex (0)).getRuleAtIndex(1)).getDeclarationAtIndex(0).getAsCSSString()); + } } diff --git a/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java b/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java index 0959b6cc..b1aa3d06 100644 --- a/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java +++ b/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java @@ -17,10 +17,12 @@ package com.helger.css.writer; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import com.helger.base.system.ENewLineMode; import org.jspecify.annotations.NonNull; import org.junit.Test; @@ -36,19 +38,21 @@ public final class CSSWriterSettingsTest extends AbstractCSSTestCase { private static void _checkDefault (@NonNull final ICSSWriterSettings aSettings) { - assertTrue (CSSWriterSettings.DEFAULT_OPTIMIZED_OUTPUT == aSettings.isOptimizedOutput ()); - assertTrue (CSSWriterSettings.DEFAULT_REMOVE_UNNECESSARY_CODE == aSettings.isRemoveUnnecessaryCode ()); - assertSame (CSSWriterSettings.DEFAULT_NEW_LINE_MODE, aSettings.getNewLineMode ()); - assertEquals (CSSWriterSettings.DEFAULT_INDENT, aSettings.getIndent (1)); - assertTrue (CSSWriterSettings.DEFAULT_QUOTE_URLS == aSettings.isQuoteURLs ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_NAMESPACE_RULES == aSettings.isWriteNamespaceRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_FONT_FACE_RULES == aSettings.isWriteFontFaceRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_KEYFRAMES_RULES == aSettings.isWriteKeyframesRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_MEDIA_RULES == aSettings.isWriteMediaRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_PAGE_RULES == aSettings.isWritePageRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_VIEWPORT_RULES == aSettings.isWriteViewportRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_SUPPORTS_RULES == aSettings.isWriteSupportsRules ()); - assertTrue (CSSWriterSettings.DEFAULT_WRITE_UNKNOWN_RULES == aSettings.isWriteUnknownRules ()); + assertFalse (aSettings.isOptimizedOutput ()); + assertFalse (aSettings.isRemoveUnnecessaryCode ()); + assertSame (ENewLineMode.UNIX, aSettings.getNewLineMode ()); + assertEquals (" ", aSettings.getIndent (1)); + assertFalse (aSettings.isQuoteURLs ()); + assertTrue (aSettings.isWriteNamespaceRules ()); + assertTrue (aSettings.isWriteNestedDeclarations ()); + assertTrue (aSettings.isWriteFontFaceRules ()); + assertTrue (aSettings.isWriteKeyframesRules ()); + assertTrue (aSettings.isWriteLayerRules ()); + assertTrue (aSettings.isWriteMediaRules ()); + assertTrue (aSettings.isWritePageRules ()); + assertTrue (aSettings.isWriteViewportRules ()); + assertTrue (aSettings.isWriteSupportsRules ()); + assertTrue (aSettings.isWriteUnknownRules ()); } @Test diff --git a/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java b/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java index aa51271f..1a48d0dd 100644 --- a/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java +++ b/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java @@ -26,6 +26,8 @@ import com.helger.css.decl.CascadingStyleSheet; import com.helger.css.reader.CSSReader; +import java.util.List; + /** * Test class for class {@link CSSWriter}. * @@ -394,4 +396,418 @@ public void testWriteCertainRules () assertNotNull (aCSS); assertEquals ("h1{color:red;margin:1px}h2{color:red;margin:1px}", aWriter.getCSSAsString (aCSS)); } + + @Test + public void testAllRulesWithPrettyPrinting () + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (""" + @charset "UTF-8"; + @import url("x.css"); + @import url("y.css"); + @namespace url(http://www.w3.org/1999/xhtml); + @namespace svg url(http://www.w3.org/2000/svg); + div { + color: red; + p: dummy; + div {} + p { + color: dummy; + } + span { + color: dummy; + margin: 0; + } + .foobar { + color: green; + element {} + #id { + color: blue + } + .class { + color: blue; + padding: 0; + } + color: white + } + color: yellow; + background-color: purple; + font-size: 12px; + @media print {} + @media print { + .print { + color: white; + } + } + @media print { + .print { + color: black; + &:hover { + color: orange; + font-size: 20px; + } + } + .pretty-print { + color: pink; + } + } + @layer state; + @layer state { .alert { color: green; } } + @layer state { + .alert { + background-color: brown; + p { + border: medium solid limegreen; + } + } + .warning { + background-color: red; + } + } + } + @font-face {} + @font-face { font-family: x; } + @font-face { + font-family: x; + src: url(x.woff2) format("woff2"); + } + @keyframes anim1 {} + @keyframes anim2 { from { opacity: 0.5; }} + @keyframes anim3 { from { opacity: 0; } to { opacity: 1; font-size: 12px; } } + @supports (display: grid) {} + @supports (display: grid) { .grid { display: grid; gap: 10px; } } + @supports (display: grid) { + .grid { + display: grid; + } + .grid { + gap: 10px; + } + } + @page {} + @page :first {} + @page :first { margin: 0; } + @page :first { + margin: 0; + padding: 0; + } + @page :first { + @top-center { content: "Preliminary edition" } + } + @page :first { + @bottom-left { } + @top-center { content: "Preliminary edition" } + @bottom-center { content: counter(page); color: violet; } + } + @viewport {} + @viewport { width: device-width; } + @viewport { + width: device-width; + height: device-height; + } + @unknown {} + @unknown { a: b; } + @unknown { a: b; c: d; } + @unknown { + a: b; + c: d; + } + @unknown { .foo { a: b; c: d; } }"""); + + assertNotNull (aCSS); + final CSSWriterSettings aSettings = new CSSWriterSettings ().setOptimizedOutput (false); + final String sPrinted = new CSSWriter (aSettings).setFooterText("end-of-file").setContentCharset ("utf-8").getCSSAsString (aCSS); + + assertEquals (""" + /* + * THIS FILE IS GENERATED - DO NOT EDIT + */ + @charset "utf-8"; + + @import url(x.css); + @import url(y.css); + + @namespace url(http://www.w3.org/1999/xhtml); + @namespace svg url(http://www.w3.org/2000/svg); + + div { + color:red; + p:dummy; + + div {} + + p { color:dummy; } + + span { + color:dummy; + margin:0; + } + + .foobar { + color:green; + + element {} + + #id { color:blue; } + + .class { + color:blue; + padding:0; + } + + color:white; + } + + color:yellow; + background-color:purple; + font-size:12px; + + @media print {} + + @media print { + .print { color:white; } + } + + @media print { + .print { + color:black; + + &:hover { + color:orange; + font-size:20px; + } + } + + .pretty-print { color:pink; } + } + + @layer state; + + @layer state { + .alert { color:green; } + } + + @layer state { + .alert { + background-color:brown; + + p { border:medium solid limegreen; } + } + .warning { background-color:red; } + } + } + + @font-face {} + + @font-face { font-family:x; } + + @font-face { + font-family:x; + src:url(x.woff2) format("woff2"); + } + + @keyframes anim1 {} + + @keyframes anim2 { + from { opacity:0.5; } + } + + @keyframes anim3 { + from { opacity:0; } + to { + opacity:1; + font-size:12px; + } + } + + @supports (display:grid) {} + + @supports (display:grid) { + .grid { + display:grid; + gap:10px; + } + } + + @supports (display:grid) { + .grid { display:grid; } + .grid { gap:10px; } + } + + @page {} + + @page :first {} + + @page :first { margin:0; } + + @page :first { + margin:0; + padding:0; + } + + @page :first { @top-center { content:"Preliminary edition"; } } + + @page :first { + @bottom-left {} + @top-center { content:"Preliminary edition"; } + @bottom-center { + content:counter(page); + color:violet; + } + } + + @viewport {} + + @viewport { width:device-width; } + + @viewport { + width:device-width; + height:device-height; + } + + @unknown {} + + @unknown { + a: b; + } + + @unknown { + a: b; c: d; + } + + @unknown { + a: b; + c: d; + } + + @unknown { + .foo { a: b; c: d; } + } + /* + * end-of-file + */ + """, sPrinted); + } + + @Test + public void testAllRulesWithOptimizedPrinting () + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (""" + @charset "UTF-8"; + @import url("x.css"); + @import url("y.css"); + @namespace url(http://www.w3.org/1999/xhtml); + @namespace svg url(http://www.w3.org/2000/svg); + div { + color: red; + p: dummy; + div {} + p { + color: dummy; + } + span { + color: dummy; + margin: 0; + } + .foobar { + color: green; + element {} + #id { + color: blue + } + .class { + color: blue; + padding: 0; + } + color: white + } + color: yellow; + background-color: purple; + font-size: 12px; + @media print {} + @media print { + .print { + color: white; + } + } + @media print { + .print { + color: black; + &:hover { + color: orange; + font-size: 20px; + } + } + .pretty-print { + color: pink; + } + } + @layer state; + @layer state { .alert { color: green; } } + @layer state { + .alert { + background-color: brown; + p { + border: medium solid limegreen; + } + } + .warning { + background-color: red; + } + } + } + @font-face {} + @font-face { font-family: x; } + @font-face { + font-family: x; + src: url(x.woff2) format("woff2"); + } + @keyframes anim1 {} + @keyframes anim2 { from { opacity: 0.5; }} + @keyframes anim3 { from { opacity: 0; } to { opacity: 1; font-size: 12px; } } + @supports (display: grid) {} + @supports (display: grid) { .grid { display: grid; gap: 10px; } } + @supports (display: grid) { + .grid { + display: grid; + } + .grid { + gap: 10px; + } + } + @page {} + @page :first {} + @page :first { margin: 0; } + @page :first { + margin: 0; + padding: 0; + } + @page :first { + @top-center { content: "Preliminary edition" } + } + @page :first { + @bottom-left { } + @top-center { content: "Preliminary edition" } + @bottom-center { content: counter(page); color: violet; } + } + @viewport {} + @viewport { width: device-width; } + @viewport { + width: device-width; + height: device-height; + } + @unknown {} + @unknown { a: b; } + @unknown { a: b; c: d; } + @unknown { + a: b; + c: d; + } + @unknown { .foo { a: b; c: d; } }"""); + + assertNotNull (aCSS); + final CSSWriterSettings aSettings = new CSSWriterSettings ().setOptimizedOutput (true); + final String sPrinted = new CSSWriter (aSettings).setFooterText("end-of-file").setContentCharset ("utf-8").getCSSAsString (aCSS); + + assertEquals ("@charset \"utf-8\";@import url(x.css);@import url(y.css);@namespace url(http://www.w3.org/1999/xhtml);@namespace svg url(http://www.w3.org/2000/svg);div{color:red;p:dummy;div{}p{color:dummy}span{color:dummy;margin:0}.foobar{color:green;element{}#id{color:blue}.class{color:blue;padding:0}color:white}color:yellow;background-color:purple;font-size:12px;@media print{}@media print{.print{color:white}}@media print{.print{color:black;&:hover{color:orange;font-size:20px}}.pretty-print{color:pink}}@layer state;@layer state{.alert{color:green}}@layer state{.alert{background-color:brown;p{border:medium solid limegreen}}.warning{background-color:red}}}@font-face{}@font-face{font-family:x}@font-face{font-family:x;src:url(x.woff2) format(\"woff2\")}@keyframes anim1{}@keyframes anim2{from{opacity:0.5}}@keyframes anim3{from{opacity:0}to{opacity:1;font-size:12px}}@supports (display:grid){}@supports (display:grid){.grid{display:grid;gap:10px}}@supports (display:grid){.grid{display:grid}.grid{gap:10px}}@page{}@page :first{}@page :first{margin:0}@page :first{margin:0;padding:0}@page :first{@top-center{content:\"Preliminary edition\"}}@page :first{@bottom-left{}@top-center{content:\"Preliminary edition\"}@bottom-center{content:counter(page);color:violet}}@viewport{}@viewport{width:device-width}@viewport{width:device-width;height:device-height}@unknown{}@unknown{a: b;}@unknown{a: b; c: d;}@unknown{a: b;\n c: d;}@unknown{.foo { a: b; c: d; }}", sPrinted); + } } From e1a2682d11fb4b1bb2198fbcdfbdd99d55f7e7f8 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 15:01:27 +0100 Subject: [PATCH 04/12] Update wpt CSS test css3-modsel-156* & is now a valid selector member. The test suite has changed the tests to use % instead of &, which is still invalid. See: https://github.com/web-platform-tests/wpt/blob/master/css/selectors/old-tests/css3-modsel-156.xml --- .../css30/bad_but_browsercompliant/css3-modsel-156.css | 2 +- .../css30/bad_but_browsercompliant/css3-modsel-156b.css | 2 +- .../css30/bad_but_browsercompliant/css3-modsel-156c.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css index 3383020e..0e811325 100644 --- a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css +++ b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156.css @@ -15,4 +15,4 @@ * limitations under the License. */ p { background: lime; } - foo & address, p { background: red; } + foo % address, p { background: red; } diff --git a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css index 4fe7dd32..2895b911 100644 --- a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css +++ b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156b.css @@ -14,5 +14,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - foo & address, p { background: red; } + foo % address, p { background: red; } p { background: lime; } diff --git a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css index 36003477..cc021182 100644 --- a/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css +++ b/ph-css/src/test/resources/testfiles/css30/bad_but_browsercompliant/css3-modsel-156c.css @@ -14,5 +14,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - foo & address, p { background: red ! important; } + foo % address, p { background: red ! important; } p { background: lime; } From 4716f2db19e11a3212d06977f15899df02292d95 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 15:35:06 +0100 Subject: [PATCH 05/12] Update CSSVisitor for nested declarations and rules --- .../com/helger/css/decl/visit/CSSVisitor.java | 84 ++++++++++++++++++- .../css/decl/visit/CSSVisitorForUrl.java | 10 +++ .../css/decl/visit/DefaultCSSVisitor.java | 11 ++- .../helger/css/decl/visit/ICSSVisitor.java | 16 ++++ .../css/decl/visit/CSSVisitorFuncTest.java | 23 +++++ .../MockCountingDeclarationsVisitor.java | 26 ++++++ ...MockCountingNestedDeclarationsVisitor.java | 36 ++++++++ 7 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java create mode 100644 ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java index 42ad533c..9223c886 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java @@ -16,6 +16,9 @@ */ package com.helger.css.decl.visit; +import com.helger.css.decl.CSSNestedDeclarations; +import com.helger.css.decl.ICSSNestedRule; +import com.helger.css.decl.IHasCSSNestedRules; import org.jspecify.annotations.NonNull; import com.helger.annotation.concurrent.Immutable; @@ -85,7 +88,7 @@ public static void visitNamespaceRule (@NonNull final CSSNamespaceRule aNamespac } /** - * 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. @@ -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..21830bfc 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,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..6d0c7a9f 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) {} - + + @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..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/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..67d52e56 --- /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 declarations = new ArrayList<>(); + + @Override + public void onDeclaration(@NonNull CSSDeclaration aDeclaration) { + m_nDeclaration++; + declarations.add(aDeclaration.getAsCSSString()); + } + + public int getDeclarationCount() { + return m_nDeclaration; + } + + public List getDeclarations() { + return declarations; + } +} diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java new file mode 100644 index 00000000..53dafa0b --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java @@ -0,0 +1,36 @@ +package com.helger.css.decl.visit; + +import com.helger.css.decl.CSSNestedDeclarations; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +class MockCountingNestedDeclarationsVisitor extends DefaultCSSVisitor { + private int m_nBeginNestedDeclarations = 0; + private int m_nEndNestedDeclarations = 0; + private final List nestedDeclaration = new ArrayList<>(); + + @Override + public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + m_nBeginNestedDeclarations++; + } + + @Override + public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + m_nEndNestedDeclarations++; + nestedDeclaration.add(aNestedDeclarations.getAsCSSString()); + } + + public int getBeginNestedDeclarationsCount() { + return m_nBeginNestedDeclarations; + } + + public int getEndNestedDeclarationsCount() { + return m_nEndNestedDeclarations; + } + + public List getNestedDeclarations() { + return nestedDeclaration; + } +} From 790a5357393c506285748d5c47019c76d083a40a Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 17:06:14 +0100 Subject: [PATCH 06/12] Add support for relative selectors in nested style rules support for relative selectors in nested style rules, e.g. .foo { > .bar { ... } } When a style rule is nested, it allows a , not just a . But at the top-level, a is not allowed, i.e. the following is invalid: > .bar { ... } To prevent the grammar from blowing up, always allow in a style rule, even at the top-level. Check for disallowed at the top level when mapping the parsed AST to the domain model. Issue a CSS interpretation error if such an invalid relative selector is encountered. --- .../css/handler/CSSNodeToDomainObject.java | 63 +++++--- ph-css/src/main/jjtree/ParserCSS30.jjt | 9 +- .../com/helger/css/decl/CSSSelectorTest.java | 148 ++++++++++++++++-- .../CollectingCSSInterpretErrorHandler.java | 38 +++++ 4 files changed, 213 insertions(+), 45 deletions(-) create mode 100644 ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java 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 36bd9e59..bf655ad1 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 @@ -779,7 +779,8 @@ private void _readStyleDeclarationList (@NonNull final CSSNode aNode, private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aNode, @NonNull final Consumer aDeclarationConsumer, - @NonNull final Consumer aNestedRuleConsumer) + @NonNull final Consumer aNestedRuleConsumer, + final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.STYLEDECLARATIONLISTWITHNESTED); // Read all contained declarations and rules @@ -806,7 +807,7 @@ private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aN { if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) aNestedRuleConsumer.accept (aNestedDeclarations); - final CSSStyleRule aRule = _createStyleRule (aChildNode); + final CSSStyleRule aRule = _createStyleRule (aChildNode, nStyleRuleCount + 1); if (aRule != null) aNestedRuleConsumer.accept (aRule); aNestedDeclarations = new CSSNestedDeclarations(); @@ -816,7 +817,7 @@ private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aN { if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) aNestedRuleConsumer.accept (aNestedDeclarations); - final CSSMediaRule aRule = _createMediaRule (aChildNode); + final CSSMediaRule aRule = _createMediaRule (aChildNode, nStyleRuleCount); if (aRule != null) aNestedRuleConsumer.accept (aRule); aNestedDeclarations = new CSSNestedDeclarations(); @@ -826,7 +827,7 @@ private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aN { if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) aNestedRuleConsumer.accept (aNestedDeclarations); - final CSSSupportsRule aRule = _createSupportsRule (aChildNode); + final CSSSupportsRule aRule = _createSupportsRule (aChildNode, nStyleRuleCount); if (aRule != null) aNestedRuleConsumer.accept (aRule); aNestedDeclarations = new CSSNestedDeclarations(); @@ -836,7 +837,7 @@ private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aN { if (aNestedDeclarations != null && aNestedDeclarations.hasDeclarations()) aNestedRuleConsumer.accept (aNestedDeclarations); - final CSSLayerRule aRule = _createLayerRule (aChildNode); + final CSSLayerRule aRule = _createLayerRule (aChildNode, nStyleRuleCount); if (aRule != null) aNestedRuleConsumer.accept (aRule); aNestedDeclarations = new CSSNestedDeclarations(); @@ -860,7 +861,7 @@ private void _readStyleDeclarationListWithNestedRules (@NonNull final CSSNode aN } @Nullable - private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) + private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.STYLERULE); final CSSStyleRule ret = new CSSStyleRule (); @@ -876,6 +877,16 @@ private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) ret.addSelector (_createSelector (aChildNode)); } + else if (ECSSNodeType.RELATIVESELECTOR.isNode (aChildNode)) + { + if (!bSelectors) + m_aErrorHandler.onCSSInterpretationError ("Found a selector after a declaration!"); + + if (nStyleRuleCount == 0) + m_aErrorHandler.onCSSInterpretationError ("Relative selectors are not allowed at the top level!"); + + ret.addSelector (_createRelativeSelector (aChildNode)); + } else { // OK, we're after the selectors @@ -883,7 +894,7 @@ private CSSStyleRule _createStyleRule (@NonNull final CSSNode aNode) if (ECSSNodeType.STYLEDECLARATIONLISTWITHNESTED.isNode (aChildNode)) { // Read all contained declarations and nested rules - _readStyleDeclarationListWithNestedRules (aChildNode, ret::addDeclaration, ret::addRule); + _readStyleDeclarationListWithNestedRules (aChildNode, ret::addDeclaration, ret::addRule, nStyleRuleCount); } else if (!ECSSNodeType.isErrorNode (aChildNode)) @@ -964,7 +975,7 @@ private CSSPageRule _createPageRule (@NonNull final CSSNode aNode) } @NonNull - private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) + private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.MEDIARULE); final CSSMediaRule ret = new CSSMediaRule (); @@ -980,7 +991,7 @@ private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) else if (ECSSNodeType.STYLERULE.isNode (aChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aChildNode, nStyleRuleCount); if (aStyleRule != null) ret.addRule (aStyleRule); } @@ -988,7 +999,7 @@ private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) { // Nested media rules are OK! - ret.addRule (_createMediaRule (aChildNode)); + ret.addRule (_createMediaRule (aChildNode, nStyleRuleCount)); } else if (ECSSNodeType.PAGERULE.isNode (aChildNode)) @@ -1004,10 +1015,10 @@ private CSSMediaRule _createMediaRule (@NonNull final CSSNode aNode) ret.addRule (_createViewportRule (aChildNode)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode)); + ret.addRule (_createSupportsRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode)); + ret.addRule (_createLayerRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) { @@ -1155,7 +1166,7 @@ private CSSFontFaceRule _createFontFaceRule (@NonNull final CSSNode aNode) } @NonNull - private CSSLayerRule _createLayerRule (@NonNull final CSSNode aNode) + private CSSLayerRule _createLayerRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.LAYERRULE); final int nChildCount = aNode.jjtGetNumChildren (); @@ -1193,19 +1204,19 @@ private CSSLayerRule _createLayerRule (@NonNull final CSSNode aNode) final CSSNode aBodyChildNode = aBodyNode.jjtGetChild (nIndex); if (ECSSNodeType.STYLERULE.isNode (aBodyChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aBodyChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aBodyChildNode, nStyleRuleCount); if (aStyleRule != null) ret.addRule (aStyleRule); } else if (ECSSNodeType.LAYERRULE.isNode (aBodyChildNode)) - ret.addRule (_createLayerRule (aBodyChildNode)); + ret.addRule (_createLayerRule (aBodyChildNode, nStyleRuleCount)); else if (ECSSNodeType.MEDIARULE.isNode (aBodyChildNode)) - ret.addRule (_createMediaRule (aBodyChildNode)); + ret.addRule (_createMediaRule (aBodyChildNode, nStyleRuleCount)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aBodyChildNode)) - ret.addRule (_createSupportsRule (aBodyChildNode)); + ret.addRule (_createSupportsRule (aBodyChildNode, nStyleRuleCount)); else if (ECSSNodeType.KEYFRAMESRULE.isNode (aBodyChildNode)) ret.addRule (_createKeyframesRule (aBodyChildNode)); @@ -1412,7 +1423,7 @@ private ICSSSupportsConditionMember _createSupportsConditionMemberRecursive (@No } @NonNull - private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) + private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode, final int nStyleRuleCount) { _expectNodeType (aNode, ECSSNodeType.SUPPORTSRULE); final CSSSupportsRule ret = new CSSSupportsRule (); @@ -1432,13 +1443,13 @@ private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) else if (ECSSNodeType.STYLERULE.isNode (aChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aChildNode, nStyleRuleCount); if (aStyleRule != null) ret.addRule (aStyleRule); } else if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) - ret.addRule (_createMediaRule (aChildNode)); + ret.addRule (_createMediaRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.PAGERULE.isNode (aChildNode)) ret.addRule (_createPageRule (aChildNode)); @@ -1453,10 +1464,10 @@ private CSSSupportsRule _createSupportsRule (@NonNull final CSSNode aNode) ret.addRule (_createViewportRule (aChildNode)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode)); + ret.addRule (_createSupportsRule (aChildNode, nStyleRuleCount)); else if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode)); + ret.addRule (_createLayerRule (aChildNode, nStyleRuleCount)); else if (!ECSSNodeType.isErrorNode (aChildNode)) m_aErrorHandler.onCSSInterpretationError ("Unsupported supports-rule child: " + @@ -1512,7 +1523,7 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a else if (ECSSNodeType.STYLERULE.isNode (aChildNode)) { - final CSSStyleRule aStyleRule = _createStyleRule (aChildNode); + final CSSStyleRule aStyleRule = _createStyleRule (aChildNode, 0); if (aStyleRule != null) ret.addRule (aStyleRule); } @@ -1521,13 +1532,13 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a ret.addRule (_createPageRule (aChildNode)); else if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) - ret.addRule (_createMediaRule (aChildNode)); + ret.addRule (_createMediaRule (aChildNode, 0)); else if (ECSSNodeType.FONTFACERULE.isNode (aChildNode)) ret.addRule (_createFontFaceRule (aChildNode)); else if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode)); + ret.addRule (_createLayerRule (aChildNode, 0)); else if (ECSSNodeType.KEYFRAMESRULE.isNode (aChildNode)) ret.addRule (_createKeyframesRule (aChildNode)); @@ -1536,7 +1547,7 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a ret.addRule (_createViewportRule (aChildNode)); else if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode)); + ret.addRule (_createSupportsRule (aChildNode, 0)); else if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) { diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index cc53973d..35a049da 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -1037,6 +1037,11 @@ void relativeSelector() : {} selector () } +void selectorOrRelativeSelector() #void : {} +{ + ( LOOKAHEAD( selectorCombinator() ) relativeSelector() | selector() ) +} + void relativeSelectorList() #void : {} { ( )* @@ -1326,11 +1331,11 @@ try { void styleRule() : {} { try{ - selector () + selectorOrRelativeSelector () ( )* ( ( )* - selector() + selectorOrRelativeSelector() ( )* )* styleDeclarationBlockWithNested() 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 06f4c460..2634be35 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 }"); - assertEquals (1, aSel.getMemberCount ()); - assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); - assertEquals ("div", aSel.getMemberAtIndex (0).getAsCSSString ()); + CSSSelector aSel = _parse("div { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals(1, aSel.getMemberCount()); + assertTrue(aSel.getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals("div", aSel.getMemberAtIndex(0).getAsCSSString()); + } - aSel = _parse ("#id { color:red }"); - assertEquals (1, aSel.getMemberCount ()); - assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); - assertEquals ("#id", aSel.getMemberAtIndex (0).getAsCSSString ()); - assertEquals ("#id", aSel.getAsCSSString ()); + @Test + public void testReadIdSelector () + { + CSSSelector aSel = _parse("#id { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals(1, aSel.getMemberCount()); + assertTrue(aSel.getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals("#id", aSel.getMemberAtIndex(0).getAsCSSString()); + assertEquals("#id", aSel.getAsCSSString()); + } - aSel = _parse ("#id div { color:red }"); + @Test + public void testReadIdSpaceCombinator () + { + CSSSelector aSel = _parse ("#id div { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (3, aSel.getMemberCount ()); assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertEquals ("#id", aSel.getMemberAtIndex (0).getAsCSSString ()); @@ -72,8 +101,13 @@ public void testRead () assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); assertEquals ("div", aSel.getMemberAtIndex (2).getAsCSSString ()); assertEquals ("#id div", aSel.getAsCSSString ()); + } - aSel = _parse ("#id ~ div { color:red }"); + @Test + public void testReadWaveDashCombinator () + { + CSSSelector aSel = _parse ("#id ~ div { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (3, aSel.getMemberCount ()); assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertEquals ("#id", aSel.getMemberAtIndex (0).getAsCSSString ()); @@ -82,15 +116,23 @@ public void testRead () assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); assertEquals ("div", aSel.getMemberAtIndex (2).getAsCSSString ()); assertEquals ("#id~div", aSel.getAsCSSString ()); + } - aSel = _parse ("&.foo { color:red }"); + @Test + public void testReadNestingSelectorAtStart() { + CSSSelector aSel = _parse ("&.foo { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (2, aSel.getMemberCount ()); assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertEquals ("&", aSel.getMemberAtIndex (0).getAsCSSString ()); assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelectorSimpleMember); assertEquals (".foo", aSel.getMemberAtIndex (1).getAsCSSString ()); + } - aSel = _parse (".foo & { color:red }"); + @Test + public void testReadNestingSelectorAtEnd() { + CSSSelector aSel = _parse (".foo & { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (3, aSel.getMemberCount ()); assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); assertEquals (".foo", aSel.getMemberAtIndex (0).getAsCSSString ()); @@ -99,4 +141,76 @@ public void testRead () assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); assertEquals ("&", aSel.getMemberAtIndex (2).getAsCSSString ()); } + + @Test + public void testReadRelativeSelectorWithinStyleRuleWithWaveDash() { + CSSStyleRule aRule = _parseRule (".foo { ~ .bar { color:red } }"); + + assertEquals(List.of(), m_aIEH.getErrors()); + + assertEquals(".foo", aRule.getSelectorAtIndex(0).getAsCSSString()); + assertTrue(aRule.getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals(1, ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorCount()); + CSSSelector aSel = ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorAtIndex(0); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals ("~", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } + + @Test + public void testReadRelativeSelectorWithinStyleRuleWithPlus() { + CSSStyleRule aRule = _parseRule (".foo { + .bar { color:red } }"); + + assertEquals(List.of(), m_aIEH.getErrors()); + + assertEquals(".foo", aRule.getSelectorAtIndex(0).getAsCSSString()); + assertTrue(aRule.getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals(1, ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorCount()); + CSSSelector aSel = ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorAtIndex(0); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals ("+", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } + + @Test + public void testReadRelativeSelectorWithinStyleRuleWithGreater() { + CSSStyleRule aRule = _parseRule (".foo { > .bar { color:red } }"); + + assertEquals(List.of(), m_aIEH.getErrors()); + + assertEquals(".foo", aRule.getSelectorAtIndex(0).getAsCSSString()); + assertTrue(aRule.getRuleAtIndex(0) instanceof CSSStyleRule); + assertEquals(1, ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorCount()); + CSSSelector aSel = ((CSSStyleRule)aRule.getRuleAtIndex(0)).getSelectorAtIndex(0); + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals (">", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } + + @Test + public void testReadRelativeSelectorAtTopLevel() { + CSSSelector aSel = _parse ("> .bar { color:red }"); + + assertEquals(List.of("Relative selectors are not allowed at the top level!"), m_aIEH.getErrors()); + + assertEquals (2, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof ECSSSelectorCombinator); + assertEquals (">", ((ECSSSelectorCombinator)aSel.getMemberAtIndex (0)).getName()); + + assertTrue (aSel.getMemberAtIndex (1) instanceof CSSSelector); + assertTrue (((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0) instanceof CSSSelectorSimpleMember); + assertEquals (".bar", ((CSSSelectorSimpleMember)((CSSSelector) aSel.getMemberAtIndex (1)).getMemberAtIndex(0)).getValue()); + } } diff --git a/ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java b/ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java new file mode 100644 index 00000000..5105a3f8 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/utils/CollectingCSSInterpretErrorHandler.java @@ -0,0 +1,38 @@ +package com.helger.css.utils; + +import com.helger.css.reader.errorhandler.ICSSInterpretErrorHandler; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public final class CollectingCSSInterpretErrorHandler implements ICSSInterpretErrorHandler { + private static final Logger LOGGER = LoggerFactory.getLogger (CollectingCSSInterpretErrorHandler.class); + + private final List m_aWarnings = new ArrayList<>(); + private final List m_aErrors = new ArrayList<>(); + + @Override + public void onCSSInterpretationWarning(@NonNull String sMessage) { + m_aWarnings.add(sMessage); + LOGGER.warn (sMessage); + } + + @Override + public void onCSSInterpretationError(@NonNull String sMessage) { + m_aErrors.add(sMessage); + LOGGER.error (sMessage); + } + + @NonNull + public List getWarnings() { + return List.copyOf(m_aWarnings); + } + + @NonNull + public List getErrors() { + return List.copyOf(m_aErrors); + } +} From 27cd08b6070ecbc8a2f8a7afc95cd51e5e7c11f7 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 18:00:18 +0100 Subject: [PATCH 07/12] Fix testfiles/css30/good/issue-gc-13.css The file is not actually good -- it's missing an rbrace! --- ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css b/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css index 92fb50a7..0253ccba 100644 --- a/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css +++ b/ph-css/src/test/resources/testfiles/css30/good/issue-gc-13.css @@ -407,4 +407,4 @@ solid #fff}#contactstable p{font-size:18px;padding:0px 0px 20px 0px;margin:0px}#contactstable h4{font-size:20px;font-weight:bold;padding:0px -0px 5px 0px;margin:0px} +0px 5px 0px;margin:0px}} From 2ce53432322141411c5082e31b238983b25a676b Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 18:10:05 +0100 Subject: [PATCH 08/12] Adjust test for different interpretation of missing rbrace in nested rule With the browser compliant flag enabled, the behavior did not change (empty string). With the browser compliant flag disabled, .class{color:red;.class{color:green}.class{color:blue} is now parsed as nothing. Previously, an error occurred at the second ".class", causing the parser to skip to the next rbrace. Now, the second ".class" is the start of a nested rule. The parser only encounters an error at the very end, when it finds a missing rbrace. The error recovery tries looking for the next rbrace, but finds none, only an EOF, causing it to discard the entire rule. --- .../test/java/com/helger/css/reader/CSSReaderFuncTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java b/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java index 18e8b0b3..c350960b 100644 --- a/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java +++ b/ph-css/src/test/java/com/helger/css/reader/CSSReaderFuncTest.java @@ -354,9 +354,10 @@ public void testSpecialCasesAsString () // Parsing problem String sCSS = ".class{color:red;.class{color:green}.class{color:blue}"; aCSS = CSSReader.readFromStringReader (sCSS, aReaderSettings); - assertNotNull (aCSS); - assertEquals (bBrowserCompliantMode ? "" : ".class{color:red}.class{color:blue}", - new CSSWriter (aWriterSettings).getCSSAsString (aCSS)); + if (bBrowserCompliantMode) + assertEquals ("", new CSSWriter (aWriterSettings).getCSSAsString (aCSS)); + else + assertNull (aCSS); sCSS = " \n/* comment */\n \n.class{color:red;}"; aCSS = CSSReader.readFromStringReader (sCSS, aReaderSettings); From 15bf1f70b6ca02db3d5327ab80aa26104531c167 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 18:23:51 +0100 Subject: [PATCH 09/12] Add test for distinction between &.foo{} and & .foo{} --- .../java/com/helger/css/decl/CSSSelectorTest.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 2634be35..4780cfdd 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 @@ -119,7 +119,7 @@ public void testReadWaveDashCombinator () } @Test - public void testReadNestingSelectorAtStart() { + public void testReadNestingSelectorAtStartWithoutSpace() { CSSSelector aSel = _parse ("&.foo { color:red }"); assertEquals(List.of(), m_aIEH.getErrors()); assertEquals (2, aSel.getMemberCount ()); @@ -129,6 +129,19 @@ public void testReadNestingSelectorAtStart() { assertEquals (".foo", aSel.getMemberAtIndex (1).getAsCSSString ()); } + @Test + public void testReadNestingSelectorAtStartWithSpace() { + CSSSelector aSel = _parse ("& .foo { color:red }"); + assertEquals(List.of(), m_aIEH.getErrors()); + assertEquals (3, aSel.getMemberCount ()); + assertTrue (aSel.getMemberAtIndex (0) instanceof CSSSelectorSimpleMember); + assertEquals ("&", aSel.getMemberAtIndex (0).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (1) instanceof ECSSSelectorCombinator); + assertEquals (" ", aSel.getMemberAtIndex (1).getAsCSSString ()); + assertTrue (aSel.getMemberAtIndex (2) instanceof CSSSelectorSimpleMember); + assertEquals (".foo", aSel.getMemberAtIndex (2).getAsCSSString ()); + } + @Test public void testReadNestingSelectorAtEnd() { CSSSelector aSel = _parse (".foo & { color:red }"); From a6c9d3f9cc92272c5e497ca99a9e3de6b16d3e11 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 18:40:20 +0100 Subject: [PATCH 10/12] Some coding style fixes Use prefix "s" for string variables Use prefix "m_" for instance fields As per https://github.com/phax/meta/blob/master/CodingStyleguide.md --- .../decl/CSSExpressionMemberLineNames.java | 12 +++++----- .../com/helger/css/decl/CSSLayerRule.java | 24 +++++++++---------- .../css/property/CSSPropertyColors.java | 4 ++-- .../css/property/CSSPropertyEnumOrColors.java | 4 ++-- .../property/CSSPropertyEnumOrNumbers.java | 4 ++-- .../MockCountingDeclarationsVisitor.java | 6 ++--- ...MockCountingNestedDeclarationsVisitor.java | 6 ++--- 7 files changed, 30 insertions(+), 30 deletions(-) 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/CSSLayerRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSLayerRule.java index e76ef713..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 @@ -74,16 +74,16 @@ public int getSelectorCount () /** * Adds a selector to the end of the selector list. - * @param aSelector The selector to be added. Must not be null. + * @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 aSelector) + public CSSLayerRule addSelector (@NonNull final String sSelector) { - ValueEnforcer.notNull (aSelector, "Selector"); + ValueEnforcer.notNull (sSelector, "Selector"); - m_aSelectors.add (aSelector); + m_aSelectors.add (sSelector); return this; } @@ -91,35 +91,35 @@ public CSSLayerRule addSelector (@NonNull final String aSelector) * 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 aSelector The selector to be added. Must not be null. + * @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 aSelector) + public CSSLayerRule addSelector (@Nonnegative final int nIndex, @NonNull final String sSelector) { ValueEnforcer.isGE0 (nIndex, "Index"); - ValueEnforcer.notNull (aSelector, "Selector"); + ValueEnforcer.notNull (sSelector, "Selector"); if (nIndex >= getSelectorCount ()) - m_aSelectors.add (aSelector); + m_aSelectors.add (sSelector); else - m_aSelectors.add (nIndex, aSelector); + m_aSelectors.add (nIndex, sSelector); return this; } /** * Remove the specified selector, if present. * - * @param aSelector The selector to be removed. Must not be null. + * @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 aSelector) + public EChange removeSelector (@NonNull final String sSelector) { - return m_aSelectors.removeObject (aSelector); + return m_aSelectors.removeObject (sSelector); } /** diff --git a/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java b/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java index c799bce8..205725dd 100644 --- a/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java +++ b/ph-css/src/main/java/com/helger/css/property/CSSPropertyColors.java @@ -98,9 +98,9 @@ public boolean isValidValue (@Nullable final String sValue) return false; // Check each value - for (final String aPart : aParts) + for (final String sPart : aParts) { - final String sTrimmedPart = aPart.trim (); + final String sTrimmedPart = sPart.trim (); if (!super.isValidValue (sTrimmedPart) && !CSSColorHelper.isColorValue (sTrimmedPart)) return false; } diff --git a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java index 893309af..b9f48852 100644 --- a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java +++ b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrColors.java @@ -104,9 +104,9 @@ public boolean isValidValue (@Nullable final String sValue) return false; // Check each value - for (final String aPart : aParts) + for (final String sPart : aParts) { - final String sTrimmedPart = aPart.trim (); + final String sTrimmedPart = sPart.trim (); if (!super.isValidValue (sTrimmedPart) && !CSSColorHelper.isColorValue (sTrimmedPart)) return false; } diff --git a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java index 168a5fd4..c27e4fed 100644 --- a/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java +++ b/ph-css/src/main/java/com/helger/css/property/CSSPropertyEnumOrNumbers.java @@ -142,9 +142,9 @@ public boolean isValidValue (@Nullable final String sValue) if (aParts.length < m_nMinNumbers || aParts.length > m_nMaxNumbers) return false; - for (final String aPart : aParts) + for (final String sPart : aParts) { - final String sTrimmedPart = aPart.trim (); + final String sTrimmedPart = sPart.trim (); if (!super.isValidValue (sTrimmedPart) && !CSSNumberHelper.isValueWithUnit (sTrimmedPart, m_bWithPercentage)) return false; } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java index 67d52e56..c60675a8 100644 --- 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 @@ -8,12 +8,12 @@ class MockCountingDeclarationsVisitor extends DefaultCSSVisitor { private int m_nDeclaration = 0; - private final List declarations = new ArrayList<>(); + private final List m_sDeclarations = new ArrayList<>(); @Override public void onDeclaration(@NonNull CSSDeclaration aDeclaration) { m_nDeclaration++; - declarations.add(aDeclaration.getAsCSSString()); + m_sDeclarations.add(aDeclaration.getAsCSSString()); } public int getDeclarationCount() { @@ -21,6 +21,6 @@ public int getDeclarationCount() { } public List getDeclarations() { - return declarations; + return m_sDeclarations; } } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java index 53dafa0b..973724a1 100644 --- a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java @@ -9,7 +9,7 @@ class MockCountingNestedDeclarationsVisitor extends DefaultCSSVisitor { private int m_nBeginNestedDeclarations = 0; private int m_nEndNestedDeclarations = 0; - private final List nestedDeclaration = new ArrayList<>(); + private final List m_sNestedDeclaration = new ArrayList<>(); @Override public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { @@ -19,7 +19,7 @@ public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDecl @Override public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { m_nEndNestedDeclarations++; - nestedDeclaration.add(aNestedDeclarations.getAsCSSString()); + m_sNestedDeclaration.add(aNestedDeclarations.getAsCSSString()); } public int getBeginNestedDeclarationsCount() { @@ -31,6 +31,6 @@ public int getEndNestedDeclarationsCount() { } public List getNestedDeclarations() { - return nestedDeclaration; + return m_sNestedDeclaration; } } From 907a01e07793afbd940a336be8b004d9ebba3fe3 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Fri, 3 Apr 2026 21:57:37 +0100 Subject: [PATCH 11/12] Implements @propery rule #121 Support property rules such as @property --canBeAnything { syntax: "*"; inherits: true; } * Add grammar rule * Add visitor methods * Add CSSPropertyRule model class --- .../com/helger/css/ICSSWriterSettings.java | 6 + .../css/decl/AbstractHasTopLevelRules.java | 52 ++++ .../com/helger/css/decl/CSSPropertyRule.java | 225 ++++++++++++++++++ .../com/helger/css/decl/ICSSTopLevelRule.java | 1 + .../com/helger/css/decl/visit/CSSVisitor.java | 185 +++++++------- .../css/decl/visit/CSSVisitorForUrl.java | 12 + .../css/decl/visit/DefaultCSSVisitor.java | 9 + .../helger/css/decl/visit/ICSSVisitor.java | 16 ++ .../css/handler/CSSNodeToDomainObject.java | 93 +++++--- .../com/helger/css/handler/ECSSNodeType.java | 1 + .../helger/css/writer/CSSWriterSettings.java | 16 ++ ph-css/src/main/jjtree/ParserCSS30.jjt | 19 ++ .../helger/css/decl/CSSFontFaceRuleTest.java | 48 ++++ .../helger/css/decl/CSSPropertyRuleTest.java | 94 ++++++++ .../css/decl/visit/CSSVisitorFuncTest.java | 56 ++++- .../MockCountingDeclarationsVisitor.java | 26 +- ...MockCountingNestedDeclarationsVisitor.java | 52 ++-- .../visit/MockCountingPageRuleVisitor.java | 38 +++ .../css/writer/CSSWriterSettingsTest.java | 1 + .../com/helger/css/writer/CSSWriterTest.java | 30 ++- 20 files changed, 800 insertions(+), 180 deletions(-) create mode 100644 ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java create mode 100644 ph-css/src/test/java/com/helger/css/decl/CSSFontFaceRuleTest.java create mode 100644 ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java create mode 100644 ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java diff --git a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java index 419b5752..93406497 100644 --- a/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java +++ b/ph-css/src/main/java/com/helger/css/ICSSWriterSettings.java @@ -23,6 +23,7 @@ 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; @@ -118,6 +119,11 @@ public interface ICSSWriterSettings */ boolean isWritePageRules (); + /** + * @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 */ diff --git a/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java b/ph-css/src/main/java/com/helger/css/decl/AbstractHasTopLevelRules.java index bc0c9bc9..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 @@ -259,6 +259,58 @@ public ICommonsList getAllLayerRules () return m_aRules.getAllMapped (CSSLayerRule.class::isInstance, CSSLayerRule.class::cast); } + /** + * Check if at least one of the top-level rules is a property rule (implementing + * {@link CSSPropertyRule}). + * + * @return true if at least one property rule is contained, false + * otherwise. + * @since 8.2.0 + */ + public boolean hasPropertyRules () + { + return m_aRules.containsAny (CSSPropertyRule.class::isInstance); + } + + /** + * Get the number of top-level rules that are property rules (implementing {@link CSSPropertyRule}). + * + * @return The number of contained property rules. Always ≥ 0. + * @since 8.2.0 + */ + @Nonnegative + public int getPropertyRuleCount () + { + return m_aRules.getCount (CSSPropertyRule.class::isInstance); + } + + /** + * Get the property rule at the specified index. + * + * @param nIndex + * The index to be resolved. Should be ≥ 0 and < {@link #getStyleRuleCount()}. + * @return The property rule at the given index, or null if an invalid index was specified. + * @since 8.2.0 + */ + @Nullable + public CSSPropertyRule getPropertyRuleAtIndex (@Nonnegative final int nIndex) + { + return m_aRules.getAtIndexMapped (CSSPropertyRule.class::isInstance, nIndex, CSSPropertyRule.class::cast); + } + + /** + * Get a list of all top-level rules that are property rules (implementing {@link CSSPropertyRule}). + * + * @return A copy of all contained property rules. Never null. + * @since 8.2.0 + */ + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllPropertyRules () + { + return m_aRules.getAllMapped (CSSPropertyRule.class::isInstance, CSSPropertyRule.class::cast); + } + /** * Check if at least one of the top-level rules is a style rule (implementing * {@link CSSStyleRule}). diff --git a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java new file mode 100644 index 00000000..6cd43269 --- /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.annotation.style.ReturnsMutableCopy; +import com.helger.base.enforce.ValueEnforcer; +import com.helger.base.hashcode.HashCodeGenerator; +import com.helger.base.state.EChange; +import com.helger.base.string.StringHelper; +import com.helger.base.tostring.ToStringGenerator; +import com.helger.collection.commons.ICommonsList; +import com.helger.css.CSSSourceLocation; +import com.helger.css.ICSSSourceLocationAware; +import com.helger.css.ICSSWriterSettings; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Represents a single @viewport rule. + * + *

Example: + * + *

@property --rotation {
+  syntax: '<angle>';
+  inherits: false;
+  initial-value: 45deg;
+}
+ * + * @author Philip Helger + * @since 8.2.0 + */ +@NotThreadSafe +public class CSSPropertyRule implements ICSSTopLevelRule, IHasCSSDeclarations , ICSSSourceLocationAware +{ + private final String m_sPropertyName; + private final CSSDeclarationContainer m_aDeclarations = new CSSDeclarationContainer (); + 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_sPropertyName = sPropertyName; + } + + /** + * @return The property name. Neither null nor empty. Always starts with --. + */ + @NonNull + @Nonempty + public String getPropertyName () + { + return m_sPropertyName; + } + + @Override + @NonNull + public CSSPropertyRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) + { + m_aDeclarations.addDeclaration (aDeclaration); + return this; + } + + @Override + @NonNull + public CSSPropertyRule addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) + { + m_aDeclarations.addDeclaration (nIndex, aNewDeclaration); + return this; + } + + @Override + @NonNull + public EChange removeDeclaration (@NonNull final CSSDeclaration aDeclaration) + { + return m_aDeclarations.removeDeclaration (aDeclaration); + } + + @Override + @NonNull + public EChange removeDeclaration (@Nonnegative final int nDeclarationIndex) + { + return m_aDeclarations.removeDeclaration (nDeclarationIndex); + } + + @Override + @NonNull + public EChange removeAllDeclarations () + { + return m_aDeclarations.removeAllDeclarations (); + } + + @Override + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllDeclarations () + { + return m_aDeclarations.getAllDeclarations (); + } + + @Override + @Nullable + public CSSDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) + { + return m_aDeclarations.getDeclarationAtIndex (nIndex); + } + + @Override + @NonNull + public CSSPropertyRule setDeclarationAtIndex (@Nonnegative final int nIndex, + @NonNull final CSSDeclaration aNewDeclaration) + { + m_aDeclarations.setDeclarationAtIndex (nIndex, aNewDeclaration); + return this; + } + + @Override + public boolean hasDeclarations () + { + return m_aDeclarations.hasDeclarations (); + } + + @Override + @Nonnegative + public int getDeclarationCount () + { + return m_aDeclarations.getDeclarationCount (); + } + + @Override + @Nullable + public CSSDeclaration getDeclarationOfPropertyName (@Nullable final String sPropertyName) + { + return m_aDeclarations.getDeclarationOfPropertyName (sPropertyName); + } + + @Override + @NonNull + @ReturnsMutableCopy + public ICommonsList getAllDeclarationsOfPropertyName (@Nullable final String sPropertyName) + { + return m_aDeclarations.getAllDeclarationsOfPropertyName (sPropertyName); + } + + @Override + @NonNull + @Nonempty + public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel) + { + // Always ignore property rules? + if (!aSettings.isWritePropertyRules ()) + return ""; + + final StringBuilder aSB = new StringBuilder ("@property "); + aSB.append (m_sPropertyName); + aSB.append (m_aDeclarations.getAsCSSString(aSettings, nIndentLevel)); + return aSB.toString (); + } + + @Override + @Nullable + public final CSSSourceLocation getSourceLocation () + { + return m_aSourceLocation; + } + + @Override + public final void setSourceLocation (@Nullable final CSSSourceLocation aSourceLocation) + { + m_aSourceLocation = aSourceLocation; + } + + @Override + public boolean equals (final Object o) + { + if (o == this) + return true; + if (o == null || !getClass ().equals (o.getClass ())) + return false; + final CSSPropertyRule rhs = (CSSPropertyRule) o; + return m_sPropertyName.equals (rhs.m_sPropertyName) && m_aDeclarations.equals (rhs.m_aDeclarations); + } + + @Override + public int hashCode () + { + return new HashCodeGenerator (this).append (m_sPropertyName).append (m_aDeclarations).getHashCode (); + } + + @Override + public String toString () + { + return new ToStringGenerator (this).append ("propertyName", m_sPropertyName) + .append ("declarations", m_aDeclarations) + .appendIfNotNull ("SourceLocation", m_aSourceLocation) + .getToString (); + } +} 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 3e983546..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 @@ -28,6 +28,7 @@ *
  • layer rules - {@link CSSLayerRule} *
  • media rules - {@link CSSMediaRule} *
  • page rules - {@link CSSPageRule} + *
  • property rules - {@link CSSPropertyRule} *
  • style rules - {@link CSSStyleRule} *
  • supports rules - {@link CSSSupportsRule} *
  • unknown rules - {@link CSSUnknownRule} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java index 9223c886..32665086 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java @@ -17,6 +17,7 @@ package com.helger.css.decl.visit; import com.helger.css.decl.CSSNestedDeclarations; +import com.helger.css.decl.CSSPropertyRule; import com.helger.css.decl.ICSSNestedRule; import com.helger.css.decl.IHasCSSNestedRules; import org.jspecify.annotations.NonNull; @@ -53,136 +54,125 @@ * @author Philip Helger */ @Immutable -public final class CSSVisitor -{ +public final class CSSVisitor { @PresentForCodeCoverage - private static final CSSVisitor INSTANCE = new CSSVisitor (); + private static final CSSVisitor INSTANCE = new CSSVisitor(); - private CSSVisitor () - {} + private CSSVisitor() { + } /** * Visit all elements of a single import rule. * - * @param aImportRule - * The import rule to visit. May not be null. - * @param aVisitor - * The visitor to use. May not be null. + * @param aImportRule The import rule to visit. May not be null. + * @param aVisitor The visitor to use. May not be null. */ - public static void visitImportRule (@NonNull final CSSImportRule aImportRule, @NonNull final ICSSVisitor aVisitor) - { - aVisitor.onImport (aImportRule); + public static void visitImportRule(@NonNull final CSSImportRule aImportRule, @NonNull final ICSSVisitor aVisitor) { + aVisitor.onImport(aImportRule); } /** * Visit all elements of a single namespace rule. * - * @param aNamespaceRule - * The namespace rule to visit. May not be null. - * @param aVisitor - * The visitor to use. May not be null. + * @param aNamespaceRule The namespace rule to visit. May not be null. + * @param aVisitor The visitor to use. May not be null. */ - public static void visitNamespaceRule (@NonNull final CSSNamespaceRule aNamespaceRule, @NonNull final ICSSVisitor aVisitor) - { - aVisitor.onNamespace (aNamespaceRule); + public static void visitNamespaceRule(@NonNull final CSSNamespaceRule aNamespaceRule, @NonNull final ICSSVisitor aVisitor) { + aVisitor.onNamespace(aNamespaceRule); } /** * Visit all declarations contained in the 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. + * @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) - { + 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); + 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); + visitAllDeclarations(aStyleRule, aVisitor); // for all nested rules - visitAllNestedRules (aStyleRule, aVisitor); - } - finally - { - aVisitor.onEndStyleRule (aStyleRule); + 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); + // for all declarations + for (final CSSDeclaration aDeclaration : aPageRule.getAllDeclarations()) + aVisitor.onDeclaration(aDeclaration); + } finally { + aVisitor.onEndPropertyRule(aPageRule); } } @@ -384,42 +374,47 @@ 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!"); } /** @@ -503,7 +498,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 @@ -520,7 +515,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 21830bfc..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); 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 6d0c7a9f..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 @@ -17,6 +17,7 @@ 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; @@ -93,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) {} diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/ICSSVisitor.java index 3d5faaae..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 @@ -17,6 +17,7 @@ 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; @@ -140,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.
    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 bf655ad1..69e37a3c 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 @@ -1323,6 +1323,32 @@ private CSSViewportRule _createViewportRule (@NonNull final CSSNode aNode) return ret; } + @NonNull + private CSSPropertyRule _createPropertyRule (@NonNull final CSSNode aNode) + { + _expectNodeType (aNode, ECSSNodeType.PROPERTYRULE); + + final String sPropertyName = aNode.getText (); + + final CSSPropertyRule ret = new CSSPropertyRule (sPropertyName); + if (m_bUseSourceLocation) + ret.setSourceLocation (aNode.getSourceLocation ()); + + for (final CSSNode aChildNode : aNode) + { + if (ECSSNodeType.STYLEDECLARATIONLIST.isNode (aChildNode)) + { + // Read all contained declarations + _readStyleDeclarationList (aChildNode, ret::addDeclaration); + } + else + if (!ECSSNodeType.isErrorNode (aChildNode)) + m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule child: " + + ECSSNodeType.getNodeName (aChildNode)); + } + return ret; + } + @NonNull private CSSNamespaceRule _createNamespaceRule (@NonNull final CSSNode aNode) { @@ -1531,48 +1557,51 @@ private void _recursiveFillCascadingStyleSheetFromNode (@NonNull final CSSNode a if (ECSSNodeType.PAGERULE.isNode (aChildNode)) ret.addRule (_createPageRule (aChildNode)); else - if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) - ret.addRule (_createMediaRule (aChildNode, 0)); + if (ECSSNodeType.PROPERTYRULE.isNode (aChildNode)) + ret.addRule (_createPropertyRule (aChildNode)); else - if (ECSSNodeType.FONTFACERULE.isNode (aChildNode)) - ret.addRule (_createFontFaceRule (aChildNode)); + if (ECSSNodeType.MEDIARULE.isNode (aChildNode)) + ret.addRule (_createMediaRule (aChildNode, 0)); else - if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) - ret.addRule (_createLayerRule (aChildNode, 0)); + if (ECSSNodeType.FONTFACERULE.isNode (aChildNode)) + ret.addRule (_createFontFaceRule (aChildNode)); else - if (ECSSNodeType.KEYFRAMESRULE.isNode (aChildNode)) - ret.addRule (_createKeyframesRule (aChildNode)); + if (ECSSNodeType.LAYERRULE.isNode (aChildNode)) + ret.addRule (_createLayerRule (aChildNode, 0)); else - if (ECSSNodeType.VIEWPORTRULE.isNode (aChildNode)) - ret.addRule (_createViewportRule (aChildNode)); + if (ECSSNodeType.KEYFRAMESRULE.isNode (aChildNode)) + ret.addRule (_createKeyframesRule (aChildNode)); else - if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) - ret.addRule (_createSupportsRule (aChildNode, 0)); + if (ECSSNodeType.VIEWPORTRULE.isNode (aChildNode)) + ret.addRule (_createViewportRule (aChildNode)); else - if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) - { - // Unknown rule indicates either - // 1. a parsing error - // 2. a non-standard rule - ret.addRule (_createUnknownRule (aChildNode)); - } + if (ECSSNodeType.SUPPORTSRULE.isNode (aChildNode)) + ret.addRule (_createSupportsRule (aChildNode, 0)); else - if (ECSSNodeType.ROOT.isNode (aChildNode)) + if (ECSSNodeType.UNKNOWNRULE.isNode (aChildNode)) { - /* - * In case a parsing error occurs (as e.g. happening in issue #41) - * and browser compliant mode is enabled, some CSS code is skipped - * and a retry happens. This retry will be a recursive stylesheet - * object that is a child of the previous stylesheet but "flattened" - * for the result object. - */ - _recursiveFillCascadingStyleSheetFromNode (aChildNode, ret); + // Unknown rule indicates either + // 1. a parsing error + // 2. a non-standard rule + ret.addRule (_createUnknownRule (aChildNode)); } else - m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + - ECSSNodeType.getNodeName (aNode) + - ": " + - ECSSNodeType.getNodeName (aChildNode)); + if (ECSSNodeType.ROOT.isNode (aChildNode)) + { + /* + * In case a parsing error occurs (as e.g. happening in issue #41) + * and browser compliant mode is enabled, some CSS code is skipped + * and a retry happens. This retry will be a recursive stylesheet + * object that is a child of the previous stylesheet but "flattened" + * for the result object. + */ + _recursiveFillCascadingStyleSheetFromNode (aChildNode, ret); + } + else + m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + + ECSSNodeType.getNodeName (aNode) + + ": " + + ECSSNodeType.getNodeName (aChildNode)); } } diff --git a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java index 004604fb..95e49ff1 100644 --- a/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java +++ b/ph-css/src/main/java/com/helger/css/handler/ECSSNodeType.java @@ -43,6 +43,7 @@ public enum ECSSNodeType STYLERULE (ParserCSS30TreeConstants.JJTSTYLERULE), IMPORTRULE (ParserCSS30TreeConstants.JJTIMPORTRULE), PAGERULE (ParserCSS30TreeConstants.JJTPAGERULE), + PROPERTYRULE (ParserCSS30TreeConstants.JJTPROPERTYRULE), MEDIARULE (ParserCSS30TreeConstants.JJTMEDIARULE), LAYERRULE (ParserCSS30TreeConstants.JJTLAYERRULE), FONTFACERULE (ParserCSS30TreeConstants.JJTFONTFACERULE), 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 dcb0c4bb..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 @@ -65,6 +65,8 @@ public class CSSWriterSettings implements ICSSWriterSettings, ICloneable | < NAMESPACE_SYM: "@namespace" > | < PAGE_SYM: "@page" > +| < PROPERTY_SYM: "@property" > | < TOPLEFTCORNER_SYM: "@top-left-corner" > | < TOPLEFT_SYM: "@top-left" > | < TOPCENTER_SYM: "@top-center" > @@ -572,6 +573,7 @@ try{ | layerRule() | fontfaceRule() | keyframesRule() + | propertyRule() | viewportRule() | supportsRule() | unknownRule() @@ -1231,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!"); } @@ -1252,6 +1255,7 @@ void styleDeclarationOrRuleWithNested() #void : {} | 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() { } @@ -1423,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!"); } ) ( )* @@ -1682,6 +1687,19 @@ void viewportRule() : {} styleDeclarationBlock() } +// +// Property rule +// https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule +// +void propertyRule() : {} +{ + + ( )* + { jjtThis.setText (token.image); } + ( )* + styleDeclarationBlock() +} + // // Supports rule // http://www.w3.org/TR/2013/CR-css3-conditional-20130404/#at-supports @@ -1738,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/CSSPropertyRuleTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java new file mode 100644 index 00000000..c7514c4d --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -0,0 +1,94 @@ +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 CSSPropertyRuleTest { + @NonNull + private static CSSPropertyRule _parse (@NonNull final String sCSS) + { + final CascadingStyleSheet aCSS = CSSReader.readFromString (sCSS); + assertNotNull (sCSS, aCSS); + assertTrue (aCSS.hasPropertyRules ()); + assertEquals (1, aCSS.getPropertyRuleCount ()); + final CSSPropertyRule ret = aCSS.getAllPropertyRules ().get (0); + assertNotNull (ret); + return ret; + } + + @Test + public void testRead1 () + { + CSSPropertyRule aSR = _parse (""" + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + """); + assertEquals ("--rotation", aSR.getPropertyName ()); + assertEquals (3, aSR.getDeclarationCount ()); + assertEquals ("syntax", aSR.getDeclarationAtIndex (0).getProperty ()); + assertEquals ("\"\"", aSR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + assertEquals ("inherits", aSR.getDeclarationAtIndex (1).getProperty ()); + assertEquals ("false", aSR.getDeclarationAtIndex (1).getExpression().getAsCSSString ()); + assertEquals ("initial-value", aSR.getDeclarationAtIndex (2).getProperty ()); + assertEquals ("45deg", aSR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + } + + @Test + public void testRead2 () + { + CSSPropertyRule aSR = _parse ("@property --rotation {}"); + assertEquals ("--rotation", aSR.getPropertyName ()); + assertEquals (0, aSR.getDeclarationCount ()); + } + + @Test + public void testRead3 () + { + // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. + CSSPropertyRule aSR = _parse ("@property --rotation { color: red;}"); + assertEquals ("--rotation", aSR.getPropertyName ()); + assertEquals (1, aSR.getDeclarationCount ()); + assertEquals ("color", aSR.getDeclarationAtIndex (0).getProperty ()); + assertEquals ("red", aSR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + } + + @Test + public void testWrite1 () + { + CSSPropertyRule aSR = _parse (""" + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + """); + assertEquals (""" + @property --rotation { + syntax:""; + inherits:false; + initial-value:45deg; + }""", aSR.getAsCSSString ()); + } + + @Test + public void testWrite2 () + { + CSSPropertyRule aSR = _parse ("@property --rotation {}"); + assertEquals ("@property --rotation {}", aSR.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 977ccd3d..1e917898 100644 --- a/ph-css/src/test/java/com/helger/css/decl/visit/CSSVisitorFuncTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/visit/CSSVisitorFuncTest.java @@ -110,22 +110,52 @@ public void testVisitConstantCSS () @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()); + 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()); + CascadingStyleSheet aSheet = CSSReader.readFromString (".foo { color: red; .bar { color: green; } color: blue; }"); + CSSStyleRule aStyleRule = aSheet.getStyleRuleAtIndex (0); + MockCountingDeclarationsVisitor aVisitor = new MockCountingDeclarationsVisitor (); + CSSVisitor.visitStyleRule (aStyleRule, aVisitor); + assertEquals(3, aVisitor.getDeclarationCount ()); + assertEquals(List.of ("color:red", "color:green", "color:blue"), aVisitor.getDeclarations ()); + } + + @Test + public void testVisitPropertyRule() { + CascadingStyleSheet aSheet = CSSReader.readFromString (""" + @property --canBeAnything { + syntax: "*"; + inherits: true; + } + + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } + + @property --defaultSize { + syntax: " | "; + inherits: true; + initial-value: 200px; + } + """); + MockCountingPageRuleVisitor aVisitor = new MockCountingPageRuleVisitor (); + CSSVisitor.visitCSS (aSheet, aVisitor); + assertEquals(3, aVisitor.getBeginPropertyRuleCount ()); + assertEquals(3, aVisitor.getEndPropertyRuleCount ()); + assertEquals(List.of ( + "@property --canBeAnything {\n syntax:\"*\";\n inherits:true;\n}", + "@property --rotation {\n syntax:\"\";\n inherits:false;\n initial-value:45deg;\n}", + "@property --defaultSize {\n syntax:\" | \";\n inherits:true;\n initial-value:200px;\n}"), aVisitor.getPropertyRules ()); } } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingDeclarationsVisitor.java index c60675a8..e4d3c1a3 100644 --- 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 @@ -7,20 +7,20 @@ import java.util.List; class MockCountingDeclarationsVisitor extends DefaultCSSVisitor { - private int m_nDeclaration = 0; - private final List m_sDeclarations = new ArrayList<>(); + private int m_nDeclaration = 0; + private final List m_sDeclarations = new ArrayList<>(); - @Override - public void onDeclaration(@NonNull CSSDeclaration aDeclaration) { - m_nDeclaration++; - m_sDeclarations.add(aDeclaration.getAsCSSString()); - } + @Override + public void onDeclaration(@NonNull CSSDeclaration aDeclaration) { + m_nDeclaration++; + m_sDeclarations.add(aDeclaration.getAsCSSString()); + } - public int getDeclarationCount() { - return m_nDeclaration; - } + public int getDeclarationCount() { + return m_nDeclaration; + } - public List getDeclarations() { - return m_sDeclarations; - } + public List getDeclarations() { + return m_sDeclarations; + } } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java index 973724a1..f1f26709 100644 --- a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingNestedDeclarationsVisitor.java @@ -7,30 +7,30 @@ import java.util.List; class MockCountingNestedDeclarationsVisitor extends DefaultCSSVisitor { - private int m_nBeginNestedDeclarations = 0; - private int m_nEndNestedDeclarations = 0; - private final List m_sNestedDeclaration = new ArrayList<>(); - - @Override - public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { - m_nBeginNestedDeclarations++; - } - - @Override - public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { - m_nEndNestedDeclarations++; - m_sNestedDeclaration.add(aNestedDeclarations.getAsCSSString()); - } - - public int getBeginNestedDeclarationsCount() { - return m_nBeginNestedDeclarations; - } - - public int getEndNestedDeclarationsCount() { - return m_nEndNestedDeclarations; - } - - public List getNestedDeclarations() { - return m_sNestedDeclaration; - } + private int m_nBeginNestedDeclarations = 0; + private int m_nEndNestedDeclarations = 0; + private final List m_sNestedDeclaration = new ArrayList<>(); + + @Override + public void onBeginNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + m_nBeginNestedDeclarations++; + } + + @Override + public void onEndNestedDeclarations(@NonNull CSSNestedDeclarations aNestedDeclarations) { + m_nEndNestedDeclarations++; + m_sNestedDeclaration.add(aNestedDeclarations.getAsCSSString()); + } + + public int getBeginNestedDeclarationsCount() { + return m_nBeginNestedDeclarations; + } + + public int getEndNestedDeclarationsCount() { + return m_nEndNestedDeclarations; + } + + public List getNestedDeclarations() { + return m_sNestedDeclaration; + } } diff --git a/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java new file mode 100644 index 00000000..0a6c4e68 --- /dev/null +++ b/ph-css/src/test/java/com/helger/css/decl/visit/MockCountingPageRuleVisitor.java @@ -0,0 +1,38 @@ +package com.helger.css.decl.visit; + +import com.helger.css.decl.CSSPropertyRule; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +class MockCountingPageRuleVisitor extends DefaultCSSVisitor { + private int m_nBeginPropertyRules = 0; + private int m_nEndPropertyRules = 0; + private final List m_sPropertyRules = new ArrayList<>(); + + @Override + public void onBeginPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + { + m_nBeginPropertyRules++; + m_sPropertyRules.add(aPropertyRule.getAsCSSString()); + } + + @Override + public void onEndPropertyRule(@NonNull CSSPropertyRule aPropertyRule) + { + m_nEndPropertyRules++; + } + + public int getBeginPropertyRuleCount() { + return m_nBeginPropertyRules; + } + + public int getEndPropertyRuleCount() { + return m_nEndPropertyRules; + } + + public List getPropertyRules() { + return m_sPropertyRules; + } +} diff --git a/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java b/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java index b1aa3d06..892efb87 100644 --- a/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java +++ b/ph-css/src/test/java/com/helger/css/writer/CSSWriterSettingsTest.java @@ -50,6 +50,7 @@ private static void _checkDefault (@NonNull final ICSSWriterSettings aSettings) assertTrue (aSettings.isWriteLayerRules ()); assertTrue (aSettings.isWriteMediaRules ()); assertTrue (aSettings.isWritePageRules ()); + assertTrue (aSettings.isWritePropertyRules ()); assertTrue (aSettings.isWriteViewportRules ()); assertTrue (aSettings.isWriteSupportsRules ()); assertTrue (aSettings.isWriteUnknownRules ()); diff --git a/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java b/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java index 1a48d0dd..f2176f83 100644 --- a/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java +++ b/ph-css/src/test/java/com/helger/css/writer/CSSWriterTest.java @@ -504,6 +504,15 @@ public void testAllRulesWithPrettyPrinting () width: device-width; height: device-height; } + @property --rotation {} + @property --rotation { + syntax: ""; + } + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } @unknown {} @unknown { a: b; } @unknown { a: b; c: d; } @@ -664,6 +673,16 @@ public void testAllRulesWithPrettyPrinting () height:device-height; } + @property --rotation {} + + @property --rotation { syntax:""; } + + @property --rotation { + syntax:""; + inherits:false; + initial-value:45deg; + } + @unknown {} @unknown { @@ -795,6 +814,15 @@ public void testAllRulesWithOptimizedPrinting () width: device-width; height: device-height; } + @property --rotation {} + @property --rotation { + syntax: ""; + } + @property --rotation { + syntax: ""; + inherits: false; + initial-value: 45deg; + } @unknown {} @unknown { a: b; } @unknown { a: b; c: d; } @@ -808,6 +836,6 @@ public void testAllRulesWithOptimizedPrinting () final CSSWriterSettings aSettings = new CSSWriterSettings ().setOptimizedOutput (true); final String sPrinted = new CSSWriter (aSettings).setFooterText("end-of-file").setContentCharset ("utf-8").getCSSAsString (aCSS); - assertEquals ("@charset \"utf-8\";@import url(x.css);@import url(y.css);@namespace url(http://www.w3.org/1999/xhtml);@namespace svg url(http://www.w3.org/2000/svg);div{color:red;p:dummy;div{}p{color:dummy}span{color:dummy;margin:0}.foobar{color:green;element{}#id{color:blue}.class{color:blue;padding:0}color:white}color:yellow;background-color:purple;font-size:12px;@media print{}@media print{.print{color:white}}@media print{.print{color:black;&:hover{color:orange;font-size:20px}}.pretty-print{color:pink}}@layer state;@layer state{.alert{color:green}}@layer state{.alert{background-color:brown;p{border:medium solid limegreen}}.warning{background-color:red}}}@font-face{}@font-face{font-family:x}@font-face{font-family:x;src:url(x.woff2) format(\"woff2\")}@keyframes anim1{}@keyframes anim2{from{opacity:0.5}}@keyframes anim3{from{opacity:0}to{opacity:1;font-size:12px}}@supports (display:grid){}@supports (display:grid){.grid{display:grid;gap:10px}}@supports (display:grid){.grid{display:grid}.grid{gap:10px}}@page{}@page :first{}@page :first{margin:0}@page :first{margin:0;padding:0}@page :first{@top-center{content:\"Preliminary edition\"}}@page :first{@bottom-left{}@top-center{content:\"Preliminary edition\"}@bottom-center{content:counter(page);color:violet}}@viewport{}@viewport{width:device-width}@viewport{width:device-width;height:device-height}@unknown{}@unknown{a: b;}@unknown{a: b; c: d;}@unknown{a: b;\n c: d;}@unknown{.foo { a: b; c: d; }}", sPrinted); + assertEquals ("@charset \"utf-8\";@import url(x.css);@import url(y.css);@namespace url(http://www.w3.org/1999/xhtml);@namespace svg url(http://www.w3.org/2000/svg);div{color:red;p:dummy;div{}p{color:dummy}span{color:dummy;margin:0}.foobar{color:green;element{}#id{color:blue}.class{color:blue;padding:0}color:white}color:yellow;background-color:purple;font-size:12px;@media print{}@media print{.print{color:white}}@media print{.print{color:black;&:hover{color:orange;font-size:20px}}.pretty-print{color:pink}}@layer state;@layer state{.alert{color:green}}@layer state{.alert{background-color:brown;p{border:medium solid limegreen}}.warning{background-color:red}}}@font-face{}@font-face{font-family:x}@font-face{font-family:x;src:url(x.woff2) format(\"woff2\")}@keyframes anim1{}@keyframes anim2{from{opacity:0.5}}@keyframes anim3{from{opacity:0}to{opacity:1;font-size:12px}}@supports (display:grid){}@supports (display:grid){.grid{display:grid;gap:10px}}@supports (display:grid){.grid{display:grid}.grid{gap:10px}}@page{}@page :first{}@page :first{margin:0}@page :first{margin:0;padding:0}@page :first{@top-center{content:\"Preliminary edition\"}}@page :first{@bottom-left{}@top-center{content:\"Preliminary edition\"}@bottom-center{content:counter(page);color:violet}}@viewport{}@viewport{width:device-width}@viewport{width:device-width;height:device-height}@property --rotation{}@property --rotation{syntax:\"\"}@property --rotation{syntax:\"\";inherits:false;initial-value:45deg}@unknown{}@unknown{a: b;}@unknown{a: b; c: d;}@unknown{a: b;\n c: d;}@unknown{.foo { a: b; c: d; }}", sPrinted); } } From 7391d3454494dbb62a9b2dde98ea7e91efe12080 Mon Sep 17 00:00:00 2001 From: Andre Wachsmuth Date: Sat, 4 Apr 2026 08:53:57 +0100 Subject: [PATCH 12/12] Change CSSPropertyRule to align with CSSOM The CSSOM exposes the syntax, initialValue, and inherits properties directly, and does not have nested child nodes. --- .../com/helger/css/decl/CSSPropertyRule.java | 168 +++++++++--------- .../com/helger/css/decl/visit/CSSVisitor.java | 3 - .../css/handler/CSSNodeToDomainObject.java | 36 +++- .../helger/css/decl/CSSPropertyRuleTest.java | 70 +++++--- .../com/helger/css/decl/CSSSelectorTest.java | 2 +- 5 files changed, 163 insertions(+), 116 deletions(-) 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 index 6cd43269..6457234e 100644 --- a/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java +++ b/ph-css/src/main/java/com/helger/css/decl/CSSPropertyRule.java @@ -19,19 +19,20 @@ import com.helger.annotation.Nonempty; import com.helger.annotation.Nonnegative; import com.helger.annotation.concurrent.NotThreadSafe; -import com.helger.annotation.style.ReturnsMutableCopy; import com.helger.base.enforce.ValueEnforcer; import com.helger.base.hashcode.HashCodeGenerator; -import com.helger.base.state.EChange; import com.helger.base.string.StringHelper; import com.helger.base.tostring.ToStringGenerator; -import com.helger.collection.commons.ICommonsList; import com.helger.css.CSSSourceLocation; import com.helger.css.ICSSSourceLocationAware; import com.helger.css.ICSSWriterSettings; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * Represents a single @viewport rule. * @@ -47,10 +48,12 @@ * @since 8.2.0 */ @NotThreadSafe -public class CSSPropertyRule implements ICSSTopLevelRule, IHasCSSDeclarations , ICSSSourceLocationAware +public class CSSPropertyRule implements ICSSTopLevelRule, ICSSSourceLocationAware { - private final String m_sPropertyName; - private final CSSDeclarationContainer m_aDeclarations = new CSSDeclarationContainer (); + private String m_sName; + private String m_sInitialValue; + private Boolean m_bInherits; + private String m_sSyntax; private CSSSourceLocation m_aSourceLocation; /** @@ -67,7 +70,7 @@ public static boolean isValidPropertyName (@NonNull @Nonempty final String sProp public CSSPropertyRule(@NonNull @Nonempty final String sPropertyName) { ValueEnforcer.isTrue (isValidPropertyName (sPropertyName), "Property name is invalid"); - m_sPropertyName = sPropertyName; + m_sName = sPropertyName; } /** @@ -75,98 +78,44 @@ public CSSPropertyRule(@NonNull @Nonempty final String sPropertyName) */ @NonNull @Nonempty - public String getPropertyName () - { - return m_sPropertyName; - } - - @Override - @NonNull - public CSSPropertyRule addDeclaration (@NonNull final CSSDeclaration aDeclaration) + public String getName () { - m_aDeclarations.addDeclaration (aDeclaration); - return this; + return m_sName; } - @Override - @NonNull - public CSSPropertyRule addDeclaration (@Nonnegative final int nIndex, @NonNull final CSSDeclaration aNewDeclaration) - { - m_aDeclarations.addDeclaration (nIndex, aNewDeclaration); - return this; + public void setName(String name) { + ValueEnforcer.isTrue (isValidPropertyName (name), "Property name is invalid"); + this.m_sName = name; } - @Override - @NonNull - public EChange removeDeclaration (@NonNull final CSSDeclaration aDeclaration) - { - return m_aDeclarations.removeDeclaration (aDeclaration); - } - @Override @NonNull - public EChange removeDeclaration (@Nonnegative final int nDeclarationIndex) + public String getInitialValue () { - return m_aDeclarations.removeDeclaration (nDeclarationIndex); + return m_sInitialValue != null ? m_sInitialValue : ""; } - @Override - @NonNull - public EChange removeAllDeclarations () - { - return m_aDeclarations.removeAllDeclarations (); + public void setInitialValue(String initialValue) { + this.m_sInitialValue = initialValue; } - @Override - @NonNull - @ReturnsMutableCopy - public ICommonsList getAllDeclarations () + public boolean isInherits () { - return m_aDeclarations.getAllDeclarations (); + return m_bInherits != null ? m_bInherits : false; } - @Override - @Nullable - public CSSDeclaration getDeclarationAtIndex (@Nonnegative final int nIndex) - { - return m_aDeclarations.getDeclarationAtIndex (nIndex); + public void setInherits(Boolean inherits) { + this.m_bInherits = inherits; } - @Override @NonNull - public CSSPropertyRule setDeclarationAtIndex (@Nonnegative final int nIndex, - @NonNull final CSSDeclaration aNewDeclaration) - { - m_aDeclarations.setDeclarationAtIndex (nIndex, aNewDeclaration); - return this; - } - - @Override - public boolean hasDeclarations () - { - return m_aDeclarations.hasDeclarations (); - } - - @Override - @Nonnegative - public int getDeclarationCount () - { - return m_aDeclarations.getDeclarationCount (); - } - - @Override - @Nullable - public CSSDeclaration getDeclarationOfPropertyName (@Nullable final String sPropertyName) + public String getSyntax () { - return m_aDeclarations.getDeclarationOfPropertyName (sPropertyName); + return m_sSyntax != null ? m_sSyntax : ""; } - @Override - @NonNull - @ReturnsMutableCopy - public ICommonsList getAllDeclarationsOfPropertyName (@Nullable final String sPropertyName) - { - return m_aDeclarations.getAllDeclarationsOfPropertyName (sPropertyName); + public void setSyntax(String syntax) { + this.m_sSyntax = syntax; } @Override @@ -178,9 +127,36 @@ public String getAsCSSString (@NonNull final ICSSWriterSettings aSettings, @Nonn if (!aSettings.isWritePropertyRules ()) return ""; + final boolean bOptimizeOutput = aSettings.isOptimizedOutput (); + final List> aDeclarations = _buildDeclarations(); + final int nCount = aDeclarations.size (); + final StringBuilder aSB = new StringBuilder ("@property "); - aSB.append (m_sPropertyName); - aSB.append (m_aDeclarations.getAsCSSString(aSettings, nIndentLevel)); + aSB.append(m_sName); + + aSB.append (bOptimizeOutput ? "{" : " {"); + if (!bOptimizeOutput && nCount == 1) + aSB.append (" "); + + int nIndex = 0; + for (final Map.Entry aDeclaration : aDeclarations) + { + if (!bOptimizeOutput && nCount > 1) + aSB.append (aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel + 1)); + aSB.append (aDeclaration.getKey ()); + aSB.append (":"); + aSB.append (aDeclaration.getValue ()); + if (!bOptimizeOutput || nIndex != aDeclarations.size() - 1) + aSB.append (";"); + ++nIndex; + } + + if (!bOptimizeOutput && nCount > 1) + aSB.append (aSettings.getNewLineString()).append (aSettings.getIndent (nIndentLevel)); + if (!bOptimizeOutput && nCount == 1) + aSB.append (" "); + aSB.append("}"); + return aSB.toString (); } @@ -205,21 +181,45 @@ public boolean equals (final Object o) if (o == null || !getClass ().equals (o.getClass ())) return false; final CSSPropertyRule rhs = (CSSPropertyRule) o; - return m_sPropertyName.equals (rhs.m_sPropertyName) && m_aDeclarations.equals (rhs.m_aDeclarations); + return m_sName.equals (rhs.m_sName) && m_sInitialValue.equals (rhs.m_sInitialValue) && m_bInherits == rhs.m_bInherits && m_sSyntax.equals (rhs.m_sSyntax); } @Override public int hashCode () { - return new HashCodeGenerator (this).append (m_sPropertyName).append (m_aDeclarations).getHashCode (); + return new HashCodeGenerator (this).append (m_sName).append (m_bInherits).append(m_sSyntax).append(m_sInitialValue).getHashCode (); } @Override public String toString () { - return new ToStringGenerator (this).append ("propertyName", m_sPropertyName) - .append ("declarations", m_aDeclarations) + return new ToStringGenerator (this).append ("propertyName", m_sName) + .append ("syntax", m_sSyntax) + .append ("inherits", m_bInherits) + .append ("initialValue", m_sInitialValue) .appendIfNotNull ("SourceLocation", m_aSourceLocation) .getToString (); } + + private List> _buildDeclarations() + { + final List> ret = new ArrayList<> (); + + if (StringHelper.isNotEmpty(m_sSyntax)) + { + ret.add (Map.entry ("syntax", m_sSyntax)); + } + + if (m_bInherits != null) + { + ret.add (Map.entry ("inherits", Boolean.toString(m_bInherits))); + } + + if (StringHelper.isNotEmpty(m_sInitialValue)) + { + ret.add (Map.entry ("initial-value", m_sInitialValue)); + } + + return ret; + } } diff --git a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java index 32665086..4a0250af 100644 --- a/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java +++ b/ph-css/src/main/java/com/helger/css/decl/visit/CSSVisitor.java @@ -168,9 +168,6 @@ public static void visitPropertyRule(@NonNull final CSSPropertyRule aPageRule, @ { try { aVisitor.onBeginPropertyRule(aPageRule); - // for all declarations - for (final CSSDeclaration aDeclaration : aPageRule.getAllDeclarations()) - aVisitor.onDeclaration(aDeclaration); } finally { aVisitor.onEndPropertyRule(aPageRule); } 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 69e37a3c..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; @@ -1334,18 +1336,40 @@ private CSSPropertyRule _createPropertyRule (@NonNull final CSSNode aNode) if (m_bUseSourceLocation) ret.setSourceLocation (aNode.getSourceLocation ()); + final List aDeclarations = new ArrayList<>(); for (final CSSNode aChildNode : aNode) { if (ECSSNodeType.STYLEDECLARATIONLIST.isNode (aChildNode)) - { // Read all contained declarations - _readStyleDeclarationList (aChildNode, ret::addDeclaration); - } + _readStyleDeclarationList (aChildNode, aDeclarations::add); else - if (!ECSSNodeType.isErrorNode (aChildNode)) - m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule child: " + - ECSSNodeType.getNodeName (aChildNode)); + if (!ECSSNodeType.isErrorNode (aChildNode)) + m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule child: " + ECSSNodeType.getNodeName (aChildNode)); } + for (final CSSDeclaration aDeclaration : aDeclarations) + switch (aDeclaration.getProperty()) + { + case "syntax": + ret.setSyntax(aDeclaration.getExpression().getAsCSSString()); + break; + case "inherits": + switch (aDeclaration.getExpression().getAsCSSString()) { + case "true": + ret.setInherits(true); + break; + case "false": + ret.setInherits(false); + break; + default: + } + break; + case "initial-value": + ret.setInitialValue(aDeclaration.getExpression().getAsCSSString()); + break; + default: + m_aErrorHandler.onCSSInterpretationError ("Unsupported property rule declaration: " + aDeclaration.getAsCSSString()); + break; + } return ret; } 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 index c7514c4d..047eacb5 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSPropertyRuleTest.java @@ -1,12 +1,16 @@ 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; @@ -16,10 +20,13 @@ * @author Philip Helger */ public class CSSPropertyRuleTest { + private final CollectingCSSInterpretErrorHandler m_aPEH = new CollectingCSSInterpretErrorHandler(); + @NonNull - private static CSSPropertyRule _parse (@NonNull final String sCSS) + private CSSPropertyRule _parse (@NonNull final String sCSS) { - final CascadingStyleSheet aCSS = CSSReader.readFromString (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 ()); @@ -38,39 +45,47 @@ public void testRead1 () initial-value: 45deg; } """); - assertEquals ("--rotation", aSR.getPropertyName ()); - assertEquals (3, aSR.getDeclarationCount ()); - assertEquals ("syntax", aSR.getDeclarationAtIndex (0).getProperty ()); - assertEquals ("\"\"", aSR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); - assertEquals ("inherits", aSR.getDeclarationAtIndex (1).getProperty ()); - assertEquals ("false", aSR.getDeclarationAtIndex (1).getExpression().getAsCSSString ()); - assertEquals ("initial-value", aSR.getDeclarationAtIndex (2).getProperty ()); - assertEquals ("45deg", aSR.getDeclarationAtIndex (2).getExpression ().getAsCSSString ()); + assertEquals ("--rotation", aSR.getName ()); + assertEquals ("\"\"", aSR.getSyntax()); + assertFalse (aSR.isInherits()); + assertEquals ("45deg", aSR.getInitialValue()); } @Test public void testRead2 () { - CSSPropertyRule aSR = _parse ("@property --rotation {}"); - assertEquals ("--rotation", aSR.getPropertyName ()); - assertEquals (0, aSR.getDeclarationCount ()); + CSSPropertyRule aPR = _parse ("@property --rotation {}"); + assertEquals ("", aPR.getSyntax()); + assertFalse (aPR.isInherits()); + assertEquals ("", aPR.getInitialValue ()); } @Test public void testRead3 () + { + CSSPropertyRule aPR = _parse ("@property --rotation { syntax: \"\";}"); + assertEquals ("--rotation", aPR.getName ()); + assertEquals ("\"\"", aPR.getSyntax()); + assertFalse (aPR.isInherits()); + assertEquals ("", aPR.getInitialValue ()); + } + + @Test + public void testRead4 () { // Unknown descriptors are invalid and ignored, but do not invalidate the @property rule. - CSSPropertyRule aSR = _parse ("@property --rotation { color: red;}"); - assertEquals ("--rotation", aSR.getPropertyName ()); - assertEquals (1, aSR.getDeclarationCount ()); - assertEquals ("color", aSR.getDeclarationAtIndex (0).getProperty ()); - assertEquals ("red", aSR.getDeclarationAtIndex (0).getExpression ().getAsCSSString ()); + CSSPropertyRule aPR = _parse ("@property --rotation { color: red;}"); + assertEquals(List.of("Unsupported property rule declaration: color:red"), m_aPEH.getErrors()); + assertEquals ("--rotation", aPR.getName ()); + assertEquals ("", aPR.getSyntax()); + assertFalse (aPR.isInherits()); + assertEquals ("", aPR.getInitialValue ()); } @Test public void testWrite1 () { - CSSPropertyRule aSR = _parse (""" + CSSPropertyRule aPR = _parse (""" @property --rotation { syntax: ""; inherits: false; @@ -82,13 +97,24 @@ public void testWrite1 () syntax:""; inherits:false; initial-value:45deg; - }""", aSR.getAsCSSString ()); + }""", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals (""" + @property --rotation{syntax:"";inherits:false;initial-value:45deg}""", aPR.getAsCSSString (new CSSWriterSettings (true))); } @Test public void testWrite2 () { - CSSPropertyRule aSR = _parse ("@property --rotation {}"); - assertEquals ("@property --rotation {}", aSR.getAsCSSString ()); + CSSPropertyRule aPR = _parse ("@property --rotation { inherits:false; }"); + assertEquals ("@property --rotation { inherits:false; }", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals ("@property --rotation{inherits:false}", aPR.getAsCSSString (new CSSWriterSettings (true))); + } + + @Test + public void testWrite3 () + { + CSSPropertyRule aPR = _parse ("@property --rotation {}"); + assertEquals ("@property --rotation {}", aPR.getAsCSSString (new CSSWriterSettings (false))); + assertEquals ("@property --rotation{}", aPR.getAsCSSString (new CSSWriterSettings (true))); } } diff --git a/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java b/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java index 4780cfdd..86f10bb8 100644 --- a/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java +++ b/ph-css/src/test/java/com/helger/css/decl/CSSSelectorTest.java @@ -36,7 +36,7 @@ */ public final class CSSSelectorTest { - private CollectingCSSInterpretErrorHandler m_aIEH = new CollectingCSSInterpretErrorHandler (); + private final CollectingCSSInterpretErrorHandler m_aIEH = new CollectingCSSInterpretErrorHandler (); @NonNull private CSSSelector _parse (@NonNull final String sCSS)