Skip to content

Commit 5d1cb9e

Browse files
committed
:has() pseudo selector support added
1 parent 7903c8b commit 5d1cb9e

File tree

9 files changed

+273
-4
lines changed

9 files changed

+273
-4
lines changed

src/main/java/org/htmlunit/cssparser/parser/condition/Condition.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ enum ConditionType {
5454
/** IS_PSEUDO_CLASS_CONDITION. */
5555
IS_PSEUDO_CLASS_CONDITION,
5656

57+
/** HAS_PSEUDO_CLASS_CONDITION. */
58+
HAS_PSEUDO_CLASS_CONDITION,
59+
5760
/** NOT_PSEUDO_CLASS_CONDITION. */
5861
NOT_PSEUDO_CLASS_CONDITION,
5962

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2019-2024 Ronald Brill.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.cssparser.parser.condition;
16+
17+
import java.io.Serializable;
18+
19+
import org.htmlunit.cssparser.parser.AbstractLocatable;
20+
import org.htmlunit.cssparser.parser.Locator;
21+
import org.htmlunit.cssparser.parser.selector.SelectorList;
22+
23+
/**
24+
* Not condition.
25+
*
26+
* @author Ronald Brill
27+
*/
28+
public class HasPseudoClassCondition extends AbstractLocatable implements Condition, Serializable {
29+
30+
private final SelectorList selectors_;
31+
private final boolean doubleColon_;
32+
33+
/**
34+
* Ctor.
35+
* @param selectors the selector list
36+
* @param locator the locator
37+
* @param doubleColon true if was prefixed by double colon
38+
*/
39+
public HasPseudoClassCondition(final SelectorList selectors, final Locator locator, final boolean doubleColon) {
40+
selectors_ = selectors;
41+
setLocator(locator);
42+
doubleColon_ = doubleColon;
43+
}
44+
45+
@Override
46+
public ConditionType getConditionType() {
47+
return ConditionType.HAS_PSEUDO_CLASS_CONDITION;
48+
}
49+
50+
/**
51+
* {@inheritDoc}
52+
*/
53+
@Override
54+
public String getLocalName() {
55+
return null;
56+
}
57+
58+
/**
59+
* {@inheritDoc}
60+
*/
61+
@Override
62+
public String getValue() {
63+
return selectors_.toString();
64+
}
65+
66+
/**
67+
* @return the list of selectors
68+
*/
69+
public SelectorList getSelectors() {
70+
return selectors_;
71+
}
72+
73+
@Override
74+
public String toString() {
75+
return (doubleColon_ ? "::" : ":") + "has(" + getValue() + ")";
76+
}
77+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2019-2024 Ronald Brill.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.cssparser.parser.selector;
16+
17+
/**
18+
* @author Ronald Brill
19+
*/
20+
public class RelativeSelector extends AbstractSelector {
21+
22+
private final char combinator_;
23+
private final Selector selector_;
24+
25+
/**
26+
* Ctor.
27+
* @param combinator the combinator to use
28+
* @param selector the selector
29+
*/
30+
public RelativeSelector(final char combinator, final Selector selector) {
31+
combinator_ = combinator;
32+
selector_ = selector;
33+
}
34+
35+
/**
36+
* @return the containing selector
37+
*/
38+
public Selector getSelector() {
39+
return selector_;
40+
}
41+
42+
@Override
43+
public SelectorType getSelectorType() {
44+
return SelectorType.RELATIVE_SELECTOR;
45+
}
46+
47+
@Override
48+
public SimpleSelector getSimpleSelector() {
49+
return null;
50+
}
51+
52+
/** {@inheritDoc} */
53+
@Override
54+
public String toString() {
55+
return combinator_ + " " + selector_.toString();
56+
}
57+
}

src/main/java/org/htmlunit/cssparser/parser/selector/Selector.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,24 @@ public interface Selector extends Locatable {
2727
enum SelectorType {
2828
/** CHILD_SELECTOR. */
2929
CHILD_SELECTOR,
30+
3031
/** DESCENDANT_SELECTOR. */
3132
DESCENDANT_SELECTOR,
33+
3234
/** DIRECT_ADJACENT_SELECTOR. */
3335
DIRECT_ADJACENT_SELECTOR,
36+
3437
/** ELEMENT_NODE_SELECTOR. */
3538
ELEMENT_NODE_SELECTOR,
39+
3640
/** GENERAL_ADJACENT_SELECTOR. */
3741
GENERAL_ADJACENT_SELECTOR,
42+
3843
/** PSEUDO_ELEMENT_SELECTOR. */
39-
PSEUDO_ELEMENT_SELECTOR
44+
PSEUDO_ELEMENT_SELECTOR,
45+
46+
/** RELATIVE_SELECTOR. */
47+
RELATIVE_SELECTOR
4048
}
4149

4250
/**

src/main/java/org/htmlunit/cssparser/parser/selector/SelectorSpecificity.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.io.Serializable;
1818

1919
import org.htmlunit.cssparser.parser.condition.Condition;
20+
import org.htmlunit.cssparser.parser.condition.HasPseudoClassCondition;
2021
import org.htmlunit.cssparser.parser.condition.IsPseudoClassCondition;
2122
import org.htmlunit.cssparser.parser.condition.NotPseudoClassCondition;
2223

@@ -144,6 +145,13 @@ private void readSelectorSpecificity(final Condition condition) {
144145
readSelectorSpecificity(selector);
145146
}
146147
return;
148+
case HAS_PSEUDO_CLASS_CONDITION:
149+
final HasPseudoClassCondition hasPseudoCondition = (HasPseudoClassCondition) condition;
150+
final SelectorList hasSelectorList = hasPseudoCondition.getSelectors();
151+
for (final Selector selector : hasSelectorList) {
152+
readSelectorSpecificity(selector);
153+
}
154+
return;
147155
case PSEUDO_CLASS_CONDITION:
148156
classCount_++;
149157
return;

src/main/javacc/CSS3Parser.jj

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import org.htmlunit.cssparser.parser.condition.ClassCondition;
4848
import org.htmlunit.cssparser.parser.condition.Condition;
4949
import org.htmlunit.cssparser.parser.condition.IdCondition;
5050
import org.htmlunit.cssparser.parser.condition.LangCondition;
51+
import org.htmlunit.cssparser.parser.condition.HasPseudoClassCondition;
5152
import org.htmlunit.cssparser.parser.condition.IsPseudoClassCondition;
5253
import org.htmlunit.cssparser.parser.condition.NotPseudoClassCondition;
5354
import org.htmlunit.cssparser.parser.condition.OneOfAttributeCondition;
@@ -63,6 +64,7 @@ import org.htmlunit.cssparser.parser.selector.DirectAdjacentSelector;
6364
import org.htmlunit.cssparser.parser.selector.ElementSelector;
6465
import org.htmlunit.cssparser.parser.selector.GeneralAdjacentSelector;
6566
import org.htmlunit.cssparser.parser.selector.PseudoElementSelector;
67+
import org.htmlunit.cssparser.parser.selector.RelativeSelector;
6668
import org.htmlunit.cssparser.parser.selector.Selector;
6769
import org.htmlunit.cssparser.parser.selector.SelectorList;
6870
import org.htmlunit.cssparser.parser.selector.SelectorListImpl;
@@ -427,6 +429,7 @@ TOKEN_MGR_DECLS :
427429
| < FUNCTION_LCH: ("ok")? "lch" <LROUND> >
428430

429431
| < FUNCTION_IS: "is" <LROUND> >
432+
| < FUNCTION_HAS: "has" <LROUND> >
430433

431434
| < CUSTOM_PROPERTY_NAME: < MINUS > <MINUS > <NMSTART> ( <NMCHAR> )* >
432435

@@ -1006,6 +1009,19 @@ char combinator() :
10061009
{ return c; }
10071010
}
10081011

1012+
char combinatorWithoutWhitespace() :
1013+
{
1014+
char c = ' ';
1015+
}
1016+
{
1017+
(
1018+
<PLUS> { c='+'; } ( <S> )*
1019+
| <GREATER> { c='>'; } ( <S> )*
1020+
| <TILDE> { c='~'; } ( <S> )*
1021+
)
1022+
{ return c; }
1023+
}
1024+
10091025
//
10101026
// unary_operator
10111027
// : '-' | PLUS
@@ -1093,6 +1109,27 @@ SelectorList selectorList() :
10931109
}
10941110
}
10951111

1112+
SelectorList relativeSelectorList() :
1113+
{
1114+
SelectorListImpl selList = new SelectorListImpl();
1115+
Selector sel;
1116+
char comb;
1117+
}
1118+
{
1119+
comb = combinatorWithoutWhitespace()
1120+
sel = selector() { selList.setLocator(sel.getLocator()); }
1121+
( <COMMA> ( <S> )*
1122+
{ selList.add(new RelativeSelector(comb, sel)); }
1123+
1124+
comb = combinatorWithoutWhitespace()
1125+
sel = selector() { selList.setLocator(sel.getLocator()); }
1126+
)*
1127+
{
1128+
selList.add(new RelativeSelector(comb, sel));
1129+
return selList;
1130+
}
1131+
}
1132+
10961133
//
10971134
// selector
10981135
// : simple_selector_sequence [ combinator simple_selector_sequence ]*
@@ -1367,6 +1404,7 @@ Object pseudo(boolean pseudoElementFound) :
13671404
String function;
13681405
boolean doubleColon = false;
13691406
SelectorList selectorList;
1407+
SelectorList relativeSelectorList;
13701408
Locator locator;
13711409
}
13721410
{
@@ -1412,6 +1450,17 @@ Object pseudo(boolean pseudoElementFound) :
14121450
}
14131451
)
14141452
|
1453+
(
1454+
t = <FUNCTION_HAS> { function = unescape(t.image, false); }
1455+
( <S> )*
1456+
relativeSelectorList = relativeSelectorList()
1457+
<RROUND>
1458+
{
1459+
if (pseudoElementFound) { throw toCSSParseException("duplicatePseudo", new String[] { function + relativeSelectorList + ")" }, locator); }
1460+
return new HasPseudoClassCondition(relativeSelectorList, locator, doubleColon);
1461+
}
1462+
)
1463+
|
14151464
(
14161465
t = <FUNCTION_LANG> { function = unescape(t.image, false); }
14171466
( <S> )*
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2019-2024 Ronald Brill.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.cssparser.parser;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
19+
import org.htmlunit.cssparser.parser.condition.Condition;
20+
import org.htmlunit.cssparser.parser.condition.Condition.ConditionType;
21+
import org.htmlunit.cssparser.parser.condition.HasPseudoClassCondition;
22+
import org.htmlunit.cssparser.parser.selector.ChildSelector;
23+
import org.htmlunit.cssparser.parser.selector.ElementSelector;
24+
import org.htmlunit.cssparser.parser.selector.RelativeSelector;
25+
import org.htmlunit.cssparser.parser.selector.Selector;
26+
import org.htmlunit.cssparser.parser.selector.Selector.SelectorType;
27+
import org.htmlunit.cssparser.parser.selector.SelectorList;
28+
import org.junit.jupiter.api.Test;
29+
30+
/**
31+
* @author Ronald Brill
32+
*/
33+
public class CSS3ParserHasSelectorTest extends AbstractCSSParserTest {
34+
35+
/**
36+
* @throws Exception if any error occurs
37+
*/
38+
@Test
39+
public void hasPlus() throws Exception {
40+
// element name
41+
final SelectorList selectors = createSelectors(":has(+ div#topic > #reference)");
42+
assertEquals("*:has(+ div#topic > *#reference)", selectors.get(0).toString());
43+
44+
assertEquals(1, selectors.size());
45+
final Selector selector = selectors.get(0);
46+
47+
assertEquals(SelectorType.ELEMENT_NODE_SELECTOR, selector.getSelectorType());
48+
49+
final ElementSelector elemSel = (ElementSelector) selector;
50+
assertEquals(1, elemSel.getConditions().size());
51+
52+
final Condition condition = elemSel.getConditions().get(0);
53+
assertEquals(ConditionType.HAS_PSEUDO_CLASS_CONDITION, condition.getConditionType());
54+
55+
final HasPseudoClassCondition pseudo = (HasPseudoClassCondition) condition;
56+
assertEquals("+ div#topic > *#reference", pseudo.getValue());
57+
assertEquals(":has(+ div#topic > *#reference)", pseudo.toString());
58+
59+
final SelectorList conditionSelectors = pseudo.getSelectors();
60+
assertEquals(1, conditionSelectors.size());
61+
final Selector conditionSelector = conditionSelectors.get(0);
62+
final RelativeSelector conditionRelativeSelector = (RelativeSelector) conditionSelector;
63+
64+
final ChildSelector conditionChildSelector = (ChildSelector) conditionRelativeSelector.getSelector();
65+
assertEquals("div#topic > *#reference", conditionChildSelector.toString());
66+
}
67+
}

src/test/java/org/htmlunit/cssparser/parser/CSS3ParserRealWorldTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,8 @@ public void bulma_1_0_2() throws Exception {
407407
+ "screen and (min-width: 1408px);"
408408
+ "screen and (min-width: 769px);"
409409
+ "screen and (min-width: 769px) and (max-width: 1023px);";
410-
realWorld("realworld/bulma_1_0_2.css", 3036, 7226, media, 13, 13);
411-
realWorld("realworld/bulma_1_0_2.min.css", 3011, 7180, media, 13, 13);
410+
realWorld("realworld/bulma_1_0_2.css", 3038, 7228, media, 11, 11);
411+
realWorld("realworld/bulma_1_0_2.min.css", 3013, 7182, media, 11, 11);
412412
}
413413

414414
/**

src/test/java/org/htmlunit/cssparser/parser/CSS3ParserTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3289,7 +3289,7 @@ public void pseudoElementsErrors() throws Exception {
32893289
checkErrorSelector("input:before:",
32903290
"Error in pseudo class or element. (Invalid token \"<EOF>\". "
32913291
+ "Was expecting one of: \"and\", \"only\", \"inherit\", \"none\", \"from\", <IDENT>, "
3292-
+ "\":\", <FUNCTION_NOT>, <FUNCTION_LANG>, <FUNCTION_IS>, <FUNCTION>.)");
3292+
+ "\":\", <FUNCTION_NOT>, <FUNCTION_LANG>, <FUNCTION_IS>, <FUNCTION_HAS>, <FUNCTION>.)");
32933293

32943294
// pseudo element not at end
32953295
checkErrorSelector("input:before:not(#test)",

0 commit comments

Comments
 (0)