Skip to content

Commit ff07bc7

Browse files
committed
:has() parser fix
1 parent 5d1cb9e commit ff07bc7

File tree

3 files changed

+197
-5
lines changed

3 files changed

+197
-5
lines changed

src/main/javacc/CSS3Parser.jj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,7 @@ char combinator() :
10091009
{ return c; }
10101010
}
10111011

1012-
char combinatorWithoutWhitespace() :
1012+
char relativeCombinator() :
10131013
{
10141014
char c = ' ';
10151015
}
@@ -1018,7 +1018,7 @@ char combinatorWithoutWhitespace() :
10181018
<PLUS> { c='+'; } ( <S> )*
10191019
| <GREATER> { c='>'; } ( <S> )*
10201020
| <TILDE> { c='~'; } ( <S> )*
1021-
)
1021+
)?
10221022
{ return c; }
10231023
}
10241024

@@ -1116,12 +1116,12 @@ SelectorList relativeSelectorList() :
11161116
char comb;
11171117
}
11181118
{
1119-
comb = combinatorWithoutWhitespace()
1119+
comb = relativeCombinator()
11201120
sel = selector() { selList.setLocator(sel.getLocator()); }
11211121
( <COMMA> ( <S> )*
11221122
{ selList.add(new RelativeSelector(comb, sel)); }
11231123

1124-
comb = combinatorWithoutWhitespace()
1124+
comb = relativeCombinator()
11251125
sel = selector() { selList.setLocator(sel.getLocator()); }
11261126
)*
11271127
{

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

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,196 @@ public void hasPlus() throws Exception {
6464
final ChildSelector conditionChildSelector = (ChildSelector) conditionRelativeSelector.getSelector();
6565
assertEquals("div#topic > *#reference", conditionChildSelector.toString());
6666
}
67+
68+
/**
69+
* @throws Exception if any error occurs
70+
*/
71+
@Test
72+
public void basic() throws Exception {
73+
parseSelectors("div:has(p)", 0, 0, 0);
74+
parseSelectors("section:has(h1)", 0, 0, 0);
75+
parseSelectors("article:has(img)", 0, 0, 0);
76+
parseSelectors("form:has(input)", 0, 0, 0);
77+
parseSelectors("ul:has(li)", 0, 0, 0);
78+
79+
parseSelectors("div:has(.warning)", 0, 0, 0);
80+
parseSelectors("section:has(#main-title)", 0, 0, 0);
81+
parseSelectors("article:has(.featured)", 0, 0, 0);
82+
parseSelectors("container:has(#sidebar)", 0, 0, 0);
83+
}
84+
85+
// /* VALID CASES - Attribute selectors within :has() */
86+
// form:has(input[required]) { border: 2px solid red; }
87+
// div:has([data-active]) { opacity: 1; }
88+
// section:has(a[href^="https"]) { padding-left: 20px; }
89+
// article:has(img[alt]) { margin: 10px; }
90+
// fieldset:has(input[type="checkbox"]) { background: #e8f4fd; }
91+
//
92+
// /* VALID CASES - Pseudo-class selectors within :has() */
93+
// div:has(a:hover) { background: lightblue; }
94+
// form:has(input:focus) { box-shadow: 0 0 5px blue; }
95+
// ul:has(li:first-child) { margin-top: 0; }
96+
// table:has(tr:nth-child(odd)) { border: 1px solid; }
97+
// section:has(p:empty) { min-height: 100px; }
98+
//
99+
// /* VALID CASES - Combinators within :has() */
100+
// div:has(> p) { padding: 10px; } /* Direct child */
101+
// section:has(h1 + p) { margin: 20px; } /* Adjacent sibling */
102+
// article:has(h2 ~ p) { line-height: 1.5; } /* General sibling */
103+
// container:has(nav a) { position: relative; } /* Descendant */
104+
//
105+
// /* VALID CASES - Multiple selectors in :has() */
106+
// div:has(p, span) { color: blue; }
107+
// section:has(h1, h2, h3) { font-weight: bold; }
108+
// article:has(img, video, iframe) { max-width: 100%; }
109+
// form:has(input, textarea, select) { padding: 15px; }
110+
//
111+
// /* VALID CASES - Nested :has() selectors */
112+
// div:has(section:has(p)) { border: 2px dashed green; }
113+
// article:has(div:has(img)) { background: lightgray; }
114+
// container:has(nav:has(ul:has(li))) { position: sticky; }
115+
//
116+
// /* VALID CASES - :has() with other pseudo-classes */
117+
// div:hover:has(p) { transform: scale(1.02); }
118+
// section:focus:has(input) { outline: 2px solid blue; }
119+
// article:first-child:has(h1) { margin-top: 0; }
120+
// li:last-child:has(a) { border-bottom: none; }
121+
//
122+
// /* VALID CASES - Complex combinators with :has() */
123+
// main > section:has(aside) { display: grid; }
124+
// nav + div:has(ul) { margin-top: 10px; }
125+
// header ~ main:has(article) { padding-top: 20px; }
126+
// .sidebar div:has(.widget) { background: white; }
127+
//
128+
// /* VALID CASES - :has() with :not() */
129+
// div:has(:not(p)) { color: red; }
130+
// section:has(h1:not(.hidden)) { display: block; }
131+
// article:has(img:not([alt])) { border: 1px solid red; }
132+
// form:has(input:not(:disabled)) { opacity: 1; }
133+
//
134+
// /* VALID CASES - :has() with :is() */
135+
// div:has(:is(p, span)) { margin: 10px; }
136+
// section:has(:is(h1, h2):not(.subtitle)) { padding: 15px; }
137+
// article:has(:is(img, video)[src]) { position: relative; }
138+
//
139+
// /* VALID CASES - Whitespace variations */
140+
// div:has(p) { color: red; }
141+
// div:has( p ) { color: blue; }
142+
// div:has(
143+
// p
144+
// ) { color: green; }
145+
// div :has(p) { color: purple; } /* Space before :has() - different meaning */
146+
//
147+
// /* VALID CASES - Single and empty selectors */
148+
// div:has(span) { display: block; }
149+
// section:has(*) { border: 1px solid; } /* Has any child */
150+
//
151+
// /* EDGE CASES - Complex attribute selectors */
152+
// div:has([data-value*="test"]) { background: yellow; }
153+
// section:has([class~="active"]) { color: green; }
154+
// article:has([id|="section"]) { margin: 20px; }
155+
// form:has([name$="email"]) { border: 1px solid blue; }
156+
//
157+
// /* EDGE CASES - Escaped characters */
158+
// div:has(.class\:name) { color: red; }
159+
// section:has(#id\.special) { background: blue; }
160+
// article:has([data-test\=value]) { padding: 10px; }
161+
//
162+
// /* EDGE CASES - Unicode selectors */
163+
// div:has(.class-über) { font-family: serif; }
164+
// section:has([data-测试]) { color: red; }
165+
// article:has(.🎉) { animation: bounce 1s; }
166+
//
167+
// /* INVALID CASES - Empty :has() */
168+
// div:has() { color: red; } /* Invalid - empty selector */
169+
// section:has( ) { color: blue; } /* Invalid - whitespace only */
170+
// article:has(,) { color: green; } /* Invalid - empty selectors */
171+
//
172+
// /* INVALID CASES - Pseudo-elements inside :has() */
173+
// div:has(p::before) { color: red; } /* Invalid - pseudo-elements not allowed */
174+
// section:has(::first-line) { background: blue; } /* Invalid */
175+
// article:has(span::after) { margin: 10px; } /* Invalid */
176+
//
177+
// /* INVALID CASES - Nested :has() with pseudo-elements */
178+
// div:has(p:has(::before)) { color: red; } /* Invalid - pseudo-elements in nested :has() */
179+
//
180+
// /* INVALID CASES - :has() inside :has() with invalid selectors */
181+
// div:has(:has()) { color: red; } /* Invalid - empty nested :has() */
182+
// section:has(p:has(::after)) { background: blue; } /* Invalid - pseudo-element in nested :has() */
183+
//
184+
// /* INVALID CASES - Syntax errors */
185+
// div:has(p { color: red; } /* Invalid - missing closing parenthesis */
186+
// section:has p) { color: blue; } /* Invalid - missing opening parenthesis */
187+
// article has(p) { color: green; } /* Invalid - missing colon */
188+
// div:has[p] { color: yellow; } /* Invalid - wrong brackets */
189+
// section:has((p)) { color: purple; } /* Invalid - double parentheses */
190+
//
191+
// /* INVALID CASES - Malformed selectors within :has() */
192+
// div:has(123) { color: red; } /* Invalid - selector starting with number */
193+
// section:has(.class--) { color: blue; } /* Invalid - malformed class name */
194+
// article:has(#) { color: green; } /* Invalid - empty ID */
195+
// form:has([=value]) { color: yellow; } /* Invalid - missing attribute name */
196+
//
197+
// /* COMPLEX VALID CASES */
198+
// .container:has(.sidebar):has(.main-content) { display: grid; grid-template-columns: 1fr 3fr; }
199+
// article:has(h1):has(p):not(:has(img)) { font-family: serif; }
200+
// section:has(> div:first-child:has(h2)) { margin-top: 2rem; }
201+
// form:has(fieldset:has(legend):has(input[required])) { border: 2px solid red; }
202+
//
203+
// /* PERFORMANCE TEST CASES - Deeply nested */
204+
// div:has(div:has(div:has(div:has(p)))) { color: red; }
205+
// section:has(article:has(header:has(h1:has(span)))) { background: lightblue; }
206+
//
207+
// /* EDGE CASES - :has() with various combinators */
208+
// main:has(> section > article) { padding: 20px; }
209+
// nav:has(ul + div) { position: relative; }
210+
// aside:has(h3 ~ p ~ div) { border-left: 3px solid; }
211+
//
212+
// /* EDGE CASES - Comments within :has() */
213+
// div:has(p /* comment */) { color: red; }
214+
// section:has(/* comment */ h1) { background: blue; }
215+
//
216+
// /* EDGE CASES - Very long selector lists in :has() */
217+
// div:has(h1, h2, h3, h4, h5, h6, p, span, div, section, article, aside, header, footer, nav, main) { font-size: 14px; }
218+
//
219+
// /* EDGE CASES - Case sensitivity */
220+
// DIV:has(P) { color: red; } /* Valid - CSS is case-insensitive for HTML elements */
221+
// div:HAS(p) { color: blue; } /* Invalid - pseudo-classes are case-sensitive */
222+
//
223+
// /* EDGE CASES - Specificity testing */
224+
// div:has(p) { color: red; } /* Specificity: 0,1,1 */
225+
// div.container:has(p.content) { color: blue; } /* Specificity: 0,2,2 */
226+
// #main div:has(p) { color: green; } /* Specificity: 1,1,1 */
227+
//
228+
// /* EDGE CASES - Multiple :has() selectors */
229+
// div:has(p):has(span) { background: yellow; }
230+
// section:has(h1):has(img):has(a) { border: 1px solid; }
231+
// article:has(.title):has(.content):has(.footer) { margin: 20px; }
232+
//
233+
// /* EDGE CASES - :has() with :where() (low specificity) */
234+
// :where(div):has(p) { color: red; }
235+
// div:has(:where(p, span)) { background: blue; }
236+
//
237+
// /* BROWSER COMPATIBILITY - Older syntax (for comparison) */
238+
// /* Note: :has() is relatively new, no legacy equivalents exist */
239+
//
240+
// /* STRESS TESTS - Complex realistic scenarios */
241+
// .card:has(.card-header):has(.card-body):not(:has(.card-footer)) {
242+
// border-bottom: 2px solid #ccc;
243+
// }
244+
//
245+
// .form-group:has(input[required]):has(label):not(:has(.error)) {
246+
// border-left: 3px solid green;
247+
// }
248+
//
249+
// .navigation:has(ul:has(li:has(a[href^="#"]))):has(.dropdown) {
250+
// position: sticky;
251+
// top: 0;
252+
// }
253+
//
254+
// /* EDGE CASES - :has() at different positions */
255+
// :has(p) div { color: red; } /* Invalid - :has() must be preceded by a selector */
256+
// *:has(p) { color: blue; } /* Valid - universal selector with :has() */
257+
// :root:has(body) { font-size: 16px; } /* Valid but unusual */
258+
67259
}

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", 3278, 6958, media, 12, 6);
537+
realWorld("realworld/alibaba-huge-index.css", 3279, 6959, media, 11, 5);
538538
}
539539

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

0 commit comments

Comments
 (0)