Skip to content

Commit 8165b9e

Browse files
committed
Parse :has selectors
Closes parcel-bundler#115
1 parent afe3bd2 commit 8165b9e

File tree

7 files changed

+309
-155
lines changed

7 files changed

+309
-155
lines changed

scripts/build-prefixes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ let cssFeatures = [
146146
'shadowdomv1',
147147
'css-rrggbbaa',
148148
'css-nesting',
149-
'css-not-sel-list'
149+
'css-not-sel-list',
150+
'css-has'
150151
];
151152

152153
let compat = new Map();

selectors/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ where
330330
*specificity += Specificity::from(max);
331331
},
332332
Component::Where(..) |
333+
Component::Has(..) |
333334
Component::ExplicitUniversalType |
334335
Component::ExplicitAnyNamespace |
335336
Component::ExplicitNoNamespace |

selectors/matching.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ where
858858
}
859859
true
860860
}),
861-
Component::Nesting => unreachable!()
861+
Component::Nesting | Component::Has(..) => unreachable!()
862862
}
863863
}
864864

selectors/parser.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,49 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> {
428428
}
429429
}
430430

431+
#[inline]
432+
fn parse_relative<'t, P>(
433+
parser: &P,
434+
input: &mut CssParser<'i, 't>,
435+
state: &mut SelectorParsingState,
436+
recovery: ParseErrorRecovery,
437+
) -> Result<Self, ParseError<'i, P::Error>>
438+
where
439+
P: Parser<'i, Impl = Impl>,
440+
{
441+
let original_state = *state;
442+
let mut values = SmallVec::new();
443+
loop {
444+
let selector = input.parse_until_before(Delimiter::Comma, |input| {
445+
let mut selector_state = original_state;
446+
let result = parse_relative_selector(parser, input, &mut selector_state);
447+
if selector_state.contains(SelectorParsingState::AFTER_NESTING) {
448+
state.insert(SelectorParsingState::AFTER_NESTING)
449+
}
450+
result
451+
});
452+
453+
let was_ok = selector.is_ok();
454+
match selector {
455+
Ok(selector) => values.push(selector),
456+
Err(err) => match recovery {
457+
ParseErrorRecovery::DiscardList => return Err(err),
458+
ParseErrorRecovery::IgnoreInvalidSelector => {},
459+
},
460+
}
461+
462+
loop {
463+
match input.next() {
464+
Err(_) => return Ok(SelectorList(values)),
465+
Ok(&Token::Comma) => break,
466+
Ok(_) => {
467+
debug_assert!(!was_ok, "Shouldn't have got a selector if getting here");
468+
},
469+
}
470+
}
471+
}
472+
}
473+
431474
/// Creates a SelectorList from a Vec of selectors. Used in tests.
432475
pub fn from_vec(v: Vec<Selector<'i, Impl>>) -> Self {
433476
SelectorList(SmallVec::from_vec(v))
@@ -1114,6 +1157,10 @@ pub enum Component<'i, Impl: SelectorImpl<'i>> {
11141157
///
11151158
/// Same comment as above re. the argument.
11161159
Is(Box<[Selector<'i, Impl>]>),
1160+
/// The `:has` pseudo-class.
1161+
///
1162+
/// https://www.w3.org/TR/selectors/#relational
1163+
Has(Box<[Selector<'i, Impl>]>),
11171164
/// An implementation-dependent pseudo-element selector.
11181165
PseudoElement(Impl::PseudoElement),
11191166
/// A nesting selector:
@@ -1587,11 +1634,12 @@ impl<'i, Impl: SelectorImpl<'i>> ToCss for Component<'i, Impl> {
15871634
write_affine(dest, a, b)?;
15881635
dest.write_char(')')
15891636
},
1590-
Is(ref list) | Where(ref list) | Negation(ref list) => {
1637+
Is(ref list) | Where(ref list) | Negation(ref list) | Has(ref list) => {
15911638
match *self {
15921639
Where(..) => dest.write_str(":where(")?,
15931640
Is(..) => dest.write_str(":is(")?,
15941641
Negation(..) => dest.write_str(":not(")?,
1642+
Has(..) => dest.write_str(":has(")?,
15951643
_ => unreachable!(),
15961644
}
15971645
serialize_selector_list(list.iter(), dest)?;
@@ -1757,6 +1805,37 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> {
17571805
}
17581806
}
17591807

1808+
fn parse_relative_selector<'i, 't, P, Impl>(
1809+
parser: &P,
1810+
input: &mut CssParser<'i, 't>,
1811+
state: &mut SelectorParsingState,
1812+
) -> Result<Selector<'i, Impl>, ParseError<'i, P::Error>>
1813+
where
1814+
P: Parser<'i, Impl = Impl>,
1815+
Impl: SelectorImpl<'i>,
1816+
{
1817+
// https://www.w3.org/TR/selectors-4/#parse-relative-selector
1818+
let s = input.state();
1819+
let combinator = match input.next()? {
1820+
Token::Delim('>') => Some(Combinator::Child),
1821+
Token::Delim('+') => Some(Combinator::NextSibling),
1822+
Token::Delim('~') => Some(Combinator::LaterSibling),
1823+
_ => {
1824+
input.reset(&s);
1825+
None
1826+
}
1827+
};
1828+
1829+
let mut selector = parse_selector(parser, input, state, NestingRequirement::None)?;
1830+
if let Some(combinator) = combinator {
1831+
// https://www.w3.org/TR/selectors/#absolutizing
1832+
selector.1.push(Component::Combinator(combinator));
1833+
selector.1.push(Component::Scope);
1834+
}
1835+
1836+
Ok(selector)
1837+
}
1838+
17601839
/// * `Err(())`: Invalid selector, abort
17611840
/// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed.
17621841
/// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`)
@@ -2315,6 +2394,27 @@ where
23152394
Ok(component(inner.0.into_vec().into_boxed_slice()))
23162395
}
23172396

2397+
fn parse_has<'i, 't, P, Impl>(
2398+
parser: &P,
2399+
input: &mut CssParser<'i, 't>,
2400+
state: &mut SelectorParsingState,
2401+
) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
2402+
where
2403+
P: Parser<'i, Impl = Impl>,
2404+
Impl: SelectorImpl<'i>,
2405+
{
2406+
let mut child_state = *state;
2407+
let inner = SelectorList::parse_relative(
2408+
parser,
2409+
input,
2410+
&mut child_state,
2411+
parser.is_and_where_error_recovery(),
2412+
)?;
2413+
if child_state.contains(SelectorParsingState::AFTER_NESTING) {
2414+
state.insert(SelectorParsingState::AFTER_NESTING)
2415+
}
2416+
Ok(Component::Has(inner.0.into_vec().into_boxed_slice()))
2417+
}
23182418
fn parse_functional_pseudo_class<'i, 't, P, Impl>(
23192419
parser: &P,
23202420
input: &mut CssParser<'i, 't>,
@@ -2332,6 +2432,7 @@ where
23322432
"nth-last-of-type" => return parse_nth_pseudo_class(parser, input, *state, Component::NthLastOfType),
23332433
"is" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Is),
23342434
"where" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Where),
2435+
"has" => return parse_has(parser, input, state),
23352436
"host" => {
23362437
if !state.allows_tree_structural_pseudo_classes() {
23372438
return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));

src/compat.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub enum Feature {
1616
CssFocusVisible,
1717
CssFocusWithin,
1818
CssGencontent,
19+
CssHas,
1920
CssInOutOfRange,
2021
CssIndeterminatePseudo,
2122
CssMarkerPseudo,
@@ -1234,6 +1235,21 @@ impl Feature {
12341235
return false
12351236
}
12361237
}
1238+
Feature::CssHas => {
1239+
if let Some(version) = browsers.safari {
1240+
if version < 984064 {
1241+
return false
1242+
}
1243+
}
1244+
if let Some(version) = browsers.ios_saf {
1245+
if version < 984064 {
1246+
return false
1247+
}
1248+
}
1249+
if browsers.android.is_some() || browsers.chrome.is_some() || browsers.edge.is_some() || browsers.firefox.is_some() || browsers.ie.is_some() || browsers.opera.is_some() || browsers.samsung.is_some() {
1250+
return false
1251+
}
1252+
}
12371253
Feature::DoublePositionGradients => {
12381254
if let Some(version) = browsers.chrome {
12391255
if version < 4653056 {

src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3656,7 +3656,15 @@ mod tests {
36563656
minify_test("::slotted(span) {}", "::slotted(span){}");
36573657
minify_test("custom-element::part(foo) {}", "custom-element::part(foo){}");
36583658
minify_test(".sm\\:text-5xl { font-size: 3rem }", ".sm\\:text-5xl{font-size:3rem}");
3659-
3659+
minify_test("a:has(> img) {}", "a:has(>img){}");
3660+
minify_test("dt:has(+ dt) {}", "dt:has(+dt){}");
3661+
minify_test("section:not(:has(h1, h2, h3, h4, h5, h6)) {}", "section:not(:has(h1,h2,h3,h4,h5,h6)){}");
3662+
minify_test(":has(.sibling ~ .target) {}", ":has(.sibling~.target){}");
3663+
minify_test(".x:has(> .a > .b) {}", ".x:has(>.a>.b){}");
3664+
minify_test(".x:has(.bar, #foo) {}", ".x:has(.bar,#foo){}");
3665+
minify_test(".x:has(span + span) {}", ".x:has(span+span){}");
3666+
minify_test("a:has(:visited) {}", "a:has(:visited){}");
3667+
36603668
prefix_test(
36613669
".test:not(.foo, .bar) {}",
36623670
indoc! {r#"

0 commit comments

Comments
 (0)