From 31e9390d781f6c377cca84f3c1febdadfe427a34 Mon Sep 17 00:00:00 2001 From: Philip Helger Date: Sun, 22 Sep 2024 18:30:16 +0200 Subject: [PATCH] Grammar changes for #101 --- .../css/handler/CSSNodeToDomainObject.java | 46 +++++--- .../com/helger/css/handler/ECSSNodeType.java | 8 +- ph-css/src/main/jjtree/ParserCSS30.jjt | 101 ++++++++++++++---- .../reader/CSSReader30SpecialFuncTest.java | 26 ++++- .../testfiles/css30/good/issue101.css | 69 ++++++++++++ 5 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 ph-css/src/test/resources/testfiles/css30/good/issue101.css 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 d5339f13..73606748 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 @@ -113,7 +113,8 @@ private CSSImportRule _createImportRule (@Nonnull final CSSNode aNode) } else if (!ECSSNodeType.MEDIALIST.isNode (aURINode, m_eVersion)) - throw new IllegalStateException ("Expected an URI or MEDIALIST node but got " + ECSSNodeType.getNodeName (aURINode, m_eVersion)); + throw new IllegalStateException ("Expected an URI or MEDIALIST node but got " + + ECSSNodeType.getNodeName (aURINode, m_eVersion)); } if (aImportURI == null) @@ -177,7 +178,11 @@ private CSSSelectorAttribute _createSelectorAttribute (@Nonnull final CSSNode aN { final int nExpectedChildCount = nOperatorIndex + 2; if (nChildren != nExpectedChildCount) - _throwUnexpectedChildrenCount (aNode, "Illegal number of children present (" + nChildren + ") - expected " + nExpectedChildCount); + _throwUnexpectedChildrenCount (aNode, + "Illegal number of children present (" + + nChildren + + ") - expected " + + nExpectedChildCount); // With operator... final CSSNode aOperator = aNode.jjtGetChild (nOperatorIndex); @@ -267,7 +272,9 @@ private ICSSSelectorMember _createSelectorMember (final CSSNode aNode) if (ECSSNodeType.NTH.isNode (aChildNode, m_eVersion)) { // Handle nth. E.g. ":nth-child(even)" or ":nth-child(3n+1)" - final CSSSelectorSimpleMember ret = new CSSSelectorSimpleMember (aNode.getText () + aChildNode.getText () + ")"); + final CSSSelectorSimpleMember ret = new CSSSelectorSimpleMember (aNode.getText () + + aChildNode.getText () + + ")"); if (m_bUseSourceLocation) ret.setSourceLocation (aNode.getSourceLocation ()); return ret; @@ -323,7 +330,8 @@ private ICSSSelectorMember _createSelectorMember (final CSSNode aNode) aNode.toString ()); } - m_aErrorHandler.onCSSInterpretationError ("Unsupported selector child: " + ECSSNodeType.getNodeName (aNode, m_eVersion)); + m_aErrorHandler.onCSSInterpretationError ("Unsupported selector child: " + + ECSSNodeType.getNodeName (aNode, m_eVersion)); return null; } @@ -381,7 +389,8 @@ private CSSExpressionMemberMathProduct _createExpressionCalcProduct (@Nonnull fi { // Must be even child count if ((nChildCount % 2) == 0) - _throwUnexpectedChildrenCount (aChildNode, "CSS math unit expected odd child count and got " + nChildCount); + _throwUnexpectedChildrenCount (aChildNode, + "CSS math unit expected odd child count and got " + nChildCount); final CSSExpressionMemberMathProduct aNestedProduct = new CSSExpressionMemberMathProduct (); for (int i = 0; i < nChildCount; ++i) @@ -588,7 +597,8 @@ private ICSSExpressionMember _createExpressionTerm (@Nonnull final CSSNode aNode return _createExpressionLineNamesTerm (aChildNode); } else - throw new IllegalStateException ("Expected an expression term but got " + ECSSNodeType.getNodeName (aChildNode, m_eVersion)); + throw new IllegalStateException ("Expected an expression term but got " + + ECSSNodeType.getNodeName (aChildNode, m_eVersion)); } @Nonnull @@ -671,7 +681,8 @@ private CSSDeclaration _createDeclaration (@Nonnull final CSSNode aNode) return ret; } - private void _readStyleDeclarationList (@Nonnull final CSSNode aNode, @Nonnull final Consumer aConsumer) + private void _readStyleDeclarationList (@Nonnull final CSSNode aNode, + @Nonnull final Consumer aConsumer) { _expectNodeType (aNode, ECSSNodeType.STYLEDECLARATIONLIST); // Read all contained declarations @@ -823,7 +834,8 @@ private CSSPageRule _createPageRule (@Nonnull final CSSNode aNode) } else if (!ECSSNodeType.isErrorNode (aChildNode, m_eVersion)) - m_aErrorHandler.onCSSInterpretationError ("Unsupported page rule child: " + ECSSNodeType.getNodeName (aChildNode, m_eVersion)); + m_aErrorHandler.onCSSInterpretationError ("Unsupported page rule child: " + + ECSSNodeType.getNodeName (aChildNode, m_eVersion)); } return ret; } @@ -963,7 +975,8 @@ private CSSMediaQuery _createMediaQuery (@Nonnull final CSSNode aNode) ret.addMediaExpression (_createMediaExpr (aChildNode)); else if (!ECSSNodeType.isErrorNode (aChildNode, m_eVersion)) - m_aErrorHandler.onCSSInterpretationError ("Unsupported media query child: " + ECSSNodeType.getNodeName (aChildNode, m_eVersion)); + m_aErrorHandler.onCSSInterpretationError ("Unsupported media query child: " + + ECSSNodeType.getNodeName (aChildNode, m_eVersion)); } return ret; } @@ -978,7 +991,8 @@ private CSSMediaExpression _createMediaExpr (@Nonnull final CSSNode aNode) final CSSNode aFeatureNode = aNode.jjtGetChild (0); if (!ECSSNodeType.MEDIAFEATURE.isNode (aFeatureNode, m_eVersion)) - throw new IllegalStateException ("Expected a media feature but got " + ECSSNodeType.getNodeName (aFeatureNode, m_eVersion)); + throw new IllegalStateException ("Expected a media feature but got " + + ECSSNodeType.getNodeName (aFeatureNode, m_eVersion)); final String sFeature = aFeatureNode.getText (); if (ECSSMediaExpressionFeature.getFromNameOrNull (sFeature) == null) m_aErrorHandler.onCSSInterpretationWarning ("Media expression uses unknown feature '" + sFeature + "'"); @@ -1118,7 +1132,8 @@ private CSSNamespaceRule _createNamespaceRule (@Nonnull final CSSNode aNode) _expectNodeType (aNode, ECSSNodeType.NAMESPACERULE); final int nChildCount = aNode.jjtGetNumChildren (); if (nChildCount < 1 || nChildCount > 2) - _throwUnexpectedChildrenCount (aNode, "Expected at least 1 child and at last 2 children but got " + nChildCount + "!"); + _throwUnexpectedChildrenCount (aNode, + "Expected at least 1 child and at last 2 children but got " + nChildCount + "!"); String sPrefix = null; int nURLIndex = 0; @@ -1204,7 +1219,8 @@ private ICSSSupportsConditionMember _createSupportsConditionMemberRecursive (@No } if (!ECSSNodeType.isErrorNode (aNode, m_eVersion)) - m_aErrorHandler.onCSSInterpretationError ("Unsupported supports-condition child: " + ECSSNodeType.getNodeName (aNode, m_eVersion)); + m_aErrorHandler.onCSSInterpretationError ("Unsupported supports-condition child: " + + ECSSNodeType.getNodeName (aNode, m_eVersion)); return null; } @@ -1286,7 +1302,8 @@ private CSSUnknownRule _createUnknownRule (@Nonnull final CSSNode aNode) return ret; } - private void _recursiveFillCascadingStyleSheetFromNode (@Nonnull final CSSNode aNode, @Nonnull final CascadingStyleSheet ret) + private void _recursiveFillCascadingStyleSheetFromNode (@Nonnull final CSSNode aNode, + @Nonnull final CascadingStyleSheet ret) { _expectNodeType (aNode, ECSSNodeType.ROOT); if (m_bUseSourceLocation) @@ -1354,7 +1371,8 @@ private void _recursiveFillCascadingStyleSheetFromNode (@Nonnull final CSSNode a m_aErrorHandler.onCSSInterpretationError ("Unsupported child of " + ECSSNodeType.getNodeName (aNode, m_eVersion) + ": " + - ECSSNodeType.getNodeName (aChildNode, m_eVersion)); + ECSSNodeType.getNodeName (aChildNode, + m_eVersion)); } } 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 08189cfc..21c17c8d 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 @@ -54,14 +54,14 @@ public enum ECSSNodeType // style rule -- selector NAMESPACEPREFIX (ParserCSS30TreeConstants.JJTNAMESPACEPREFIX), ELEMENTNAME (ParserCSS30TreeConstants.JJTELEMENTNAME), - HASH (ParserCSS30TreeConstants.JJTHASH), + HASH (ParserCSS30TreeConstants.JJTIDSELECTOR), CLASS (ParserCSS30TreeConstants.JJTCLASS), - PSEUDO (ParserCSS30TreeConstants.JJTPSEUDO), + PSEUDO (ParserCSS30TreeConstants.JJTPSEUDOCLASSSELECTOR), HOST (ParserCSS30TreeConstants.JJTHOST), HOSTCONTEXT (ParserCSS30TreeConstants.JJTHOSTCONTEXT), SLOTTED (ParserCSS30TreeConstants.JJTSLOTTED), - NEGATION (ParserCSS30TreeConstants.JJTNEGATION), - ATTRIB (ParserCSS30TreeConstants.JJTATTRIB), + NEGATION (ParserCSS30TreeConstants.JJTFUNCNOT), + ATTRIB (ParserCSS30TreeConstants.JJTATTRIBUTESELECTOR), ATTRIBOPERATOR (ParserCSS30TreeConstants.JJTATTRIBOPERATOR), ATTRIBVALUE (ParserCSS30TreeConstants.JJTATTRIBVALUE), SELECTORCOMBINATOR (ParserCSS30TreeConstants.JJTSELECTORCOMBINATOR), diff --git a/ph-css/src/main/jjtree/ParserCSS30.jjt b/ph-css/src/main/jjtree/ParserCSS30.jjt index a9aae592..256aff2c 100644 --- a/ph-css/src/main/jjtree/ParserCSS30.jjt +++ b/ph-css/src/main/jjtree/ParserCSS30.jjt @@ -301,7 +301,10 @@ TOKEN : | < FUNCTION_CALC: "calc(" | "-" "-calc(" > | < FUNCTION_NOT: ":not(" > +| < FUNCTION_IS: "is(" > +| < FUNCTION_HAS: "has(" > | < FUNCTION_HOST: "host(" > +| < FUNCTION_WHERE: "where(" > | < FUNCTION_HOSTCONTEXT: "host-context(" > | < FUNCTION_SLOTTED: "slotted(" > | < FUNCTION_NTH: "nth-child(" @@ -961,12 +964,12 @@ void typeSelector() #void : {} elementName() } -void hash() : {} +void idSelector() : {} { { jjtThis.setText (token.image); } } -void _class() #Class : +void classSelector() #Class : { String sIdentifier; } { @@ -992,7 +995,7 @@ void attribValue() : ) } -void attrib() : {} +void attributeSelector() : {} { ( )* @@ -1058,11 +1061,52 @@ void pseudoSlotted () #slotted : {} )* } -void pseudo() : {} +void relativeSelector() : {} +{ + ( selectorCombinator() + ( )* + )? + selector () + ( )* + ( + ( )* + selector() + ( )* + )* +} + +void relativeSelectorList() #void : {} +{ + ( relativeSelector() )* +} + +void pseudoHas() #has : {} +{ + ( )* + relativeSelectorList() +} + +void pseudoIs() #is : {} +{ + ( )* + selector() +} + +void pseudoWhere() #where : {} +{ + ( )* + selector() +} + +void pseudoElementSelector() : {} +{ + { jjtThis.setText (":"); } + pseudoClassSelector() +} + +void pseudoClassSelector() : {} { { jjtThis.setText (":"); } - /* Extension for "::-moz-selection */ - ( { jjtThis.appendText (":"); } )? ( { jjtThis.appendText (token.image); } pseudoNth() // do not append because of expression! @@ -1075,6 +1119,15 @@ void pseudo() : {} | { jjtThis.appendText (token.image); } pseudoSlotted() // do not append because of expression! + | { jjtThis.setText (token.image); } + pseudoHas() + // do not append because of expression! + | { jjtThis.setText (token.image); } + pseudoIs() + // do not append because of expression! + | { jjtThis.setText (token.image); } + pseudoWhere() + // do not append because of expression! | LOOKAHEAD( ) { jjtThis.appendText (token.image); } ( )* @@ -1084,7 +1137,7 @@ void pseudo() : {} ) } -void negation() : {} +void funcNot() : {} { { jjtThis.setText (":not("); } ( )* @@ -1103,19 +1156,31 @@ void simpleSelectorSequence() #void : {} { LOOKAHEAD(2) ( typeSelector() - ( hash() - | _class() - | attrib() - | pseudo() - | negation() + ( idSelector() + | classSelector() + | attributeSelector() + | pseudoClassSelector() + | funcNot() )* +// ( LOOKAHEAD(2) +// pseudoElementSelector() +// ( pseudoClassSelector() )* +// )* ) - | ( hash() - | _class() - | attrib() - | pseudo() - | negation() + | ( idSelector() + | classSelector() + | attributeSelector() + | pseudoClassSelector() + | funcNot() )+ +// ( LOOKAHEAD(2) +// pseudoElementSelector() +// ( pseudoClassSelector() )* +// )* +// | LOOKAHEAD(2) +// ( pseudoElementSelector() +// ( pseudoClassSelector() )* +// )+ // Extension for CSS animations (e.g. 50%) | } @@ -1157,7 +1222,7 @@ void property() : { errorDeprecatedProperty (aPrefixToken); } /* leaving jjtThis.text null is handled inside the code */ ( )* - ) + ) } void important() : {} diff --git a/ph-css/src/test/java/com/helger/css/reader/CSSReader30SpecialFuncTest.java b/ph-css/src/test/java/com/helger/css/reader/CSSReader30SpecialFuncTest.java index 226b0d95..72d508e6 100644 --- a/ph-css/src/test/java/com/helger/css/reader/CSSReader30SpecialFuncTest.java +++ b/ph-css/src/test/java/com/helger/css/reader/CSSReader30SpecialFuncTest.java @@ -67,10 +67,12 @@ public void testReadSpecialGood () assertNotNull (aCSS); assertEquals ("--a", aCSS.getStyleRuleAtIndex (0).getDeclarationAtIndex (0).getProperty ()); - assertEquals ("background-color:var(--A)", aCSS.getStyleRuleAtIndex (0).getDeclarationAtIndex (1).getAsCSSString ()); + assertEquals ("background-color:var(--A)", + aCSS.getStyleRuleAtIndex (0).getDeclarationAtIndex (1).getAsCSSString ()); assertEquals ("--A", aCSS.getStyleRuleAtIndex (1).getDeclarationAtIndex (0).getProperty ()); - assertEquals ("background-color:var(--A)", aCSS.getStyleRuleAtIndex (1).getDeclarationAtIndex (1).getAsCSSString ()); + assertEquals ("background-color:var(--A)", + aCSS.getStyleRuleAtIndex (1).getDeclarationAtIndex (1).getAsCSSString ()); final String sCSS = new CSSWriter (eVersion, false).getCSSAsString (aCSS); assertNotNull (sCSS); @@ -306,7 +308,10 @@ public void testReadSpecialBadButRecoverable () final ECSSVersion eVersion = ECSSVersion.CSS30; final Charset aCharset = StandardCharsets.UTF_8; final File aFile = new File ("src/test/resources/testfiles/css30/bad_but_recoverable_and_browsercompliant/test-string.css"); - final CascadingStyleSheet aCSS = CSSReader.readFromFile (aFile, aCharset, eVersion, aErrors.and (new LoggingCSSParseErrorHandler ())); + final CascadingStyleSheet aCSS = CSSReader.readFromFile (aFile, + aCharset, + eVersion, + aErrors.and (new LoggingCSSParseErrorHandler ())); assertNotNull (aFile.getAbsolutePath (), aCSS); } @@ -325,7 +330,8 @@ public void testReadWithBOM () ECSSVersion.CSS30, new DoNothingCSSParseErrorHandler ()); assertNotNull ("Failed to read with BOM " + eBOM, aCSS); - assertEquals (".class{color:red}.class{color:blue}", new CSSWriter (ECSSVersion.CSS30, true).getCSSAsString (aCSS)); + assertEquals (".class{color:red}.class{color:blue}", + new CSSWriter (ECSSVersion.CSS30, true).getCSSAsString (aCSS)); } } } @@ -376,4 +382,16 @@ public void testReadFootnote () assertEquals ("@footnote", footnoteBlock.getPageMarginSymbol ()); assertEquals (1, footnoteBlock.getDeclarationCount ()); } + + @Test + public void testIssue101 () + { + final ECSSVersion eVersion = ECSSVersion.CSS30; + final Charset aCharset = StandardCharsets.UTF_8; + final File aFile = new File ("src/test/resources/testfiles/css30/good/issue101.css"); + final CascadingStyleSheet aCSS = CSSReader.readFromFile (aFile, aCharset, eVersion); + assertNotNull (aCSS); + assertEquals (2, aCSS.getRuleCount ()); + assertEquals (2, aCSS.getStyleRuleCount ()); + } } diff --git a/ph-css/src/test/resources/testfiles/css30/good/issue101.css b/ph-css/src/test/resources/testfiles/css30/good/issue101.css new file mode 100644 index 00000000..01bd2718 --- /dev/null +++ b/ph-css/src/test/resources/testfiles/css30/good/issue101.css @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2014-2024 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. + */ +@namespace foo url(http://www.example.com); + +h1, +h2 { + margin: 0 0 1rem 0; +} + +h1:has(+ h2) { + margin: 0 0 0.25rem 0; +} + +/* +*|*:is(:hover, :focus) {} + +*|*:is(*:hover, *:focus) {} +*/ + +*:not(FOO) {} + +html|*:not(:link):not(:visited) {} + + +a:not(:hover) { + text-decoration: none; +} + +nav a { + /* Has no effect */ + text-decoration: underline; +} + +a:where(:not(:hover)) { + text-decoration: none; +} + +nav a { + /* Works now! */ + text-decoration: underline; +} + +a:has(> img) {} + +dt:has(+ dt) {} + +section:not(:has(h1, h2, h3, h4, h5, h6)) {} + +section:has(:not(h1, h2, h3, h4, h5, h6)) {} + +foo|h1 { color: blue } /* first rule */ +foo|* { color: yellow } /* second rule */ +|h1 { color: red } /* ...*/ +*|h1 { color: green } +h1 { color: green }