Skip to content

Commit 0cccb40

Browse files
committed
:where() support
1 parent ff07bc7 commit 0cccb40

File tree

6 files changed

+339
-2
lines changed

6 files changed

+339
-2
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+
/** WHERE_PSEUDO_CLASS_CONDITION. */
58+
WHERE_PSEUDO_CLASS_CONDITION,
59+
5760
/** HAS_PSEUDO_CLASS_CONDITION. */
5861
HAS_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 WherePseudoClassCondition 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 WherePseudoClassCondition(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.WHERE_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_ ? "::" : ":") + "where(" + getValue() + ")";
76+
}
77+
}

src/main/javacc/CSS3Parser.jj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import org.htmlunit.cssparser.parser.condition.PrefixAttributeCondition;
5656
import org.htmlunit.cssparser.parser.condition.PseudoClassCondition;
5757
import org.htmlunit.cssparser.parser.condition.SubstringAttributeCondition;
5858
import org.htmlunit.cssparser.parser.condition.SuffixAttributeCondition;
59+
import org.htmlunit.cssparser.parser.condition.WherePseudoClassCondition;
5960
import org.htmlunit.cssparser.parser.media.MediaQuery;
6061
import org.htmlunit.cssparser.parser.media.MediaQueryList;
6162
import org.htmlunit.cssparser.parser.selector.ChildSelector;
@@ -429,6 +430,7 @@ TOKEN_MGR_DECLS :
429430
| < FUNCTION_LCH: ("ok")? "lch" <LROUND> >
430431

431432
| < FUNCTION_IS: "is" <LROUND> >
433+
| < FUNCTION_WHERE: "where" <LROUND> >
432434
| < FUNCTION_HAS: "has" <LROUND> >
433435

434436
| < CUSTOM_PROPERTY_NAME: < MINUS > <MINUS > <NMSTART> ( <NMCHAR> )* >
@@ -1450,6 +1452,17 @@ Object pseudo(boolean pseudoElementFound) :
14501452
}
14511453
)
14521454
|
1455+
(
1456+
t = <FUNCTION_WHERE> { function = unescape(t.image, false); }
1457+
( <S> )*
1458+
selectorList = selectorList()
1459+
<RROUND>
1460+
{
1461+
if (pseudoElementFound) { throw toCSSParseException("duplicatePseudo", new String[] { function + selectorList + ")" }, locator); }
1462+
return new WherePseudoClassCondition(selectorList, locator, doubleColon);
1463+
}
1464+
)
1465+
|
14531466
(
14541467
t = <FUNCTION_HAS> { function = unescape(t.image, false); }
14551468
( <S> )*

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ public void alibabaHugeIndex() throws Exception {
534534
+ "screen and (max-width: 480px);"
535535
+ "screen and (max-width: 767px);"
536536
+ "screen and (max-width: 768px);";
537-
realWorld("realworld/alibaba-huge-index.css", 3279, 6959, media, 11, 5);
537+
realWorld("realworld/alibaba-huge-index.css", 3284, 6984, media, 6, 0);
538538
}
539539

540540
private void realWorld(final String resourceName, final int rules, final int properties,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3289,7 +3289,8 @@ 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_HAS>, <FUNCTION>.)");
3292+
+ "\":\", <FUNCTION_NOT>, <FUNCTION_LANG>, "
3293+
+ "<FUNCTION_IS>, <FUNCTION_WHERE>, <FUNCTION_HAS>, <FUNCTION>.)");
32933294

32943295
// pseudo element not at end
32953296
checkErrorSelector("input:before:not(#test)",
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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.WherePseudoClassCondition;
22+
import org.htmlunit.cssparser.parser.selector.ElementSelector;
23+
import org.htmlunit.cssparser.parser.selector.Selector;
24+
import org.htmlunit.cssparser.parser.selector.Selector.SelectorType;
25+
import org.htmlunit.cssparser.parser.selector.SelectorList;
26+
import org.junit.jupiter.api.Test;
27+
28+
/**
29+
* @author Ronald Brill
30+
*/
31+
public class CSS3ParserWhereSelectorTest extends AbstractCSSParserTest {
32+
33+
/**
34+
* @throws Exception if any error occurs
35+
*/
36+
@Test
37+
public void isElementType() throws Exception {
38+
// element name
39+
final SelectorList selectors = parseSelectors(":where(ol, ul)", 0, 0, 0);
40+
assertEquals("*:where(ol, ul)", selectors.get(0).toString());
41+
42+
assertEquals(1, selectors.size());
43+
final Selector selector = selectors.get(0);
44+
45+
assertEquals(SelectorType.ELEMENT_NODE_SELECTOR, selector.getSelectorType());
46+
47+
final ElementSelector elemSel = (ElementSelector) selector;
48+
assertEquals(1, elemSel.getConditions().size());
49+
50+
final Condition condition = elemSel.getConditions().get(0);
51+
assertEquals(ConditionType.WHERE_PSEUDO_CLASS_CONDITION, condition.getConditionType());
52+
53+
final WherePseudoClassCondition pseudo = (WherePseudoClassCondition) condition;
54+
assertEquals("ol, ul", pseudo.getValue());
55+
assertEquals(":where(ol, ul)", pseudo.toString());
56+
57+
final SelectorList conditionSelectors = pseudo.getSelectors();
58+
assertEquals(2, conditionSelectors.size());
59+
final Selector conditionSelector = conditionSelectors.get(0);
60+
final ElementSelector conditionElemSelector = (ElementSelector) conditionSelector;
61+
assertEquals("ol", conditionElemSelector.getElementName());
62+
}
63+
64+
/**
65+
* @throws Exception if any error occurs
66+
*/
67+
@Test
68+
public void basic() throws Exception {
69+
parseSelectors(":where(h1, h2, h3)", 0, 0, 0);
70+
parseSelectors(":where(.class1, .class2)", 0, 0, 0);
71+
parseSelectors(":where(#id1, #id2, #id3)", 0, 0, 0);
72+
parseSelectors(":where(div, span, p)", 0, 0, 0);
73+
parseSelectors(":where([data-attr], [title])", 0, 0, 0);
74+
parseSelectors(":where(input, textarea, select)", 0, 0, 0);
75+
}
76+
77+
/**
78+
* @throws Exception if any error occurs
79+
*/
80+
@Test
81+
public void pseudoClassCombinations() throws Exception {
82+
parseSelectors(":where(a:hover, a:focus)", 0, 0, 0);
83+
parseSelectors(":where(button:disabled, input:disabled)", 0, 0, 0);
84+
parseSelectors(":where(li:first-child, li:last-child)", 0, 0, 0);
85+
parseSelectors(":where(tr:nth-child(odd), tr:nth-child(even))", 0, 0, 0);
86+
}
87+
88+
/**
89+
* @throws Exception if any error occurs
90+
*/
91+
@Test
92+
public void complex() throws Exception {
93+
parseSelectors(":where(.nav > li, .menu > li)", 0, 0, 0);
94+
parseSelectors(":where(article h1, section h1)", 0, 0, 0);
95+
parseSelectors(":where(.sidebar .widget, .footer .widget)", 0, 0, 0);
96+
parseSelectors(":where(form input[type=\"text\"], form input[type=\"email\"])", 0, 0, 0);
97+
}
98+
99+
/**
100+
* @throws Exception if any error occurs
101+
*/
102+
@Test
103+
public void nested() throws Exception {
104+
parseSelectors(":where(:where(h1, h2), :where(h3, h4))", 0, 0, 0);
105+
parseSelectors(":where(article h1, section h1)", 0, 0, 0);
106+
parseSelectors(":where(.sidebar .widget, .footer .widget)", 0, 0, 0);
107+
parseSelectors(":where(.container :where(.item, .element), .wrapper :where(.item, .element))", 0, 0, 0);
108+
}
109+
110+
/**
111+
* @throws Exception if any error occurs
112+
*/
113+
@Test
114+
public void descendant() throws Exception {
115+
parseSelectors("div :where(p, span)", 0, 0, 0);
116+
parseSelectors(".container :where(.item, .box)", 0, 0, 0);
117+
parseSelectors("article :where(h1, h2, h3)", 0, 0, 0);
118+
}
119+
120+
/**
121+
* @throws Exception if any error occurs
122+
*/
123+
@Test
124+
public void adjacentGeneralSibling() throws Exception {
125+
parseSelectors(":where(h1, h2) + p", 0, 0, 0);
126+
parseSelectors(":where(.alert, .warning) ~ div", 0, 0, 0);
127+
parseSelectors(":where(img, video) + figcaption", 0, 0, 0);
128+
}
129+
130+
/**
131+
* @throws Exception if any error occurs
132+
*/
133+
@Test
134+
public void multiple() throws Exception {
135+
parseSelectors(":where(main, aside) > :where(section, article)", 0, 0, 0);
136+
parseSelectors(":where(.header, .footer) .nav :where(a, button)", 0, 0, 0);
137+
}
138+
139+
/**
140+
* @throws Exception if any error occurs
141+
*/
142+
@Test
143+
public void attribute() throws Exception {
144+
parseSelectors(":where(input[required], textarea[required])", 0, 0, 0);
145+
parseSelectors(":where([data-theme=\"dark\"], [data-theme=\"night\"])", 0, 0, 0);
146+
parseSelectors(":where(a[href^=\"http\"], a[href^=\"mailto\"])", 0, 0, 0);
147+
}
148+
149+
/**
150+
* @throws Exception if any error occurs
151+
*/
152+
@Test
153+
public void pseudo() throws Exception {
154+
parseSelectors(":where(h1, h2, h3)::before", 0, 0, 0);
155+
parseSelectors(":where(blockquote, q)::after", 0, 0, 0);
156+
}
157+
158+
/**
159+
* @throws Exception if any error occurs
160+
*/
161+
@Test
162+
public void whitespace() throws Exception {
163+
parseSelectors(":where(h1,h2,h3)", 0, 0, 0);
164+
parseSelectors(":where( h1 , h2 , h3 )", 0, 0, 0);
165+
parseSelectors(":where( h1,\n h2,\n h3\n )", 0, 0, 0);
166+
}
167+
168+
/**
169+
* @throws Exception if any error occurs
170+
*/
171+
@Test
172+
public void single() throws Exception {
173+
parseSelectors(":where(div)", 0, 0, 0);
174+
parseSelectors(":where(.single-class)", 0, 0, 0);
175+
}
176+
177+
/**
178+
* @throws Exception if any error occurs
179+
*/
180+
@Test
181+
public void emptyAndWhitespace() throws Exception {
182+
parseSelectors(":where()", 1, 0, 0);
183+
parseSelectors(":where( )", 1, 0, 0);
184+
parseSelectors(":where(,)", 1, 0, 0);
185+
parseSelectors(":where(h1,)", 1, 0, 0);
186+
parseSelectors(":where(h1,,h2)", 1, 0, 0);
187+
}
188+
189+
/**
190+
* @throws Exception if any error occurs
191+
*/
192+
@Test
193+
public void syntaxErrors() throws Exception {
194+
// parseSelectors(":where(h1 h2)", 1, 0, 0);
195+
// parseSelectors(":is h1, h2", 1, 0, 0);
196+
parseSelectors("is(h1, h2)", 1, 0, 0);
197+
parseSelectors(":where((h1, h2))", 1, 0, 0);
198+
parseSelectors(":is[h1, h2]", 1, 0, 0);
199+
200+
parseSelectors(":where(h1, h2", 1, 0, 0);
201+
parseSelectors(":is h1, h2)", 1, 0, 0);
202+
}
203+
204+
/**
205+
* @throws Exception if any error occurs
206+
*/
207+
@Test
208+
public void pseudoElementsInside() throws Exception {
209+
parseSelectors(":where(p::before, p::after)", 0, 0, 0);
210+
parseSelectors(":where(::first-line, ::first-letter)", 0, 0, 0);
211+
parseSelectors(":where(div::placeholder, input::placeholder)", 0, 0, 0);
212+
}
213+
214+
/**
215+
* @throws Exception if any error occurs
216+
*/
217+
@Test
218+
public void invalidSelectorsWithin() throws Exception {
219+
parseSelectors(":where(123, h2)", 1, 0, 0);
220+
//parseSelectors(":where(.class--, h2)", 1, 0, 0);
221+
parseSelectors(":where(#, h2)", 1, 0, 0);
222+
parseSelectors(":where([attr=], h2)", 1, 0, 0);
223+
}
224+
225+
/**
226+
* @throws Exception if any error occurs
227+
*/
228+
@Test
229+
public void caseSensitive() throws Exception {
230+
parseSelectors(":where(h2)", 0, 0, 0);
231+
parseSelectors(":where(h2)", 0, 0, 0);
232+
parseSelectors(":where(h2)", 0, 0, 0);
233+
}
234+
235+
/**
236+
* @throws Exception if any error occurs
237+
*/
238+
@Test
239+
public void comment() throws Exception {
240+
parseSelectors(":where(h1 /* comment */, h2)", 0, 0, 0);
241+
parseSelectors(":where(/* comment */ h1, h2)", 0, 0, 0);
242+
}
243+
}

0 commit comments

Comments
 (0)