Skip to content

Commit f479601

Browse files
committed
Support pseudo classes on ::-webkit-scrollbar pseudo elements
Fixes parcel-bundler#122
1 parent 37522bc commit f479601

File tree

4 files changed

+151
-8
lines changed

4 files changed

+151
-8
lines changed

selectors/parser.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ pub trait PseudoElement<'i>: Sized + ToCss {
3636
fn valid_after_slotted(&self) -> bool {
3737
false
3838
}
39+
40+
fn is_webkit_scrollbar(&self) -> bool {
41+
false
42+
}
3943
}
4044

4145
/// A trait that represents a pseudo-class.
@@ -51,6 +55,10 @@ pub trait NonTSPseudoClass<'i>: Sized + ToCss {
5155
/// https://drafts.csswg.org/selectors-4/#useraction-pseudos
5256
fn is_user_action_state(&self) -> bool;
5357

58+
fn is_webkit_scrollbar_state(&self) -> bool {
59+
false
60+
}
61+
5462
fn visit<V>(&self, _visitor: &mut V) -> bool
5563
where
5664
V: SelectorVisitor<'i, Impl = Self::Impl>,
@@ -73,7 +81,7 @@ fn to_ascii_lowercase<'i>(s: CowRcStr<'i>) -> CowRcStr<'i> {
7381

7482
bitflags! {
7583
/// Flags that indicate at which point of parsing a selector are we.
76-
struct SelectorParsingState: u8 {
84+
struct SelectorParsingState: u16 {
7785
/// Whether we should avoid adding default namespaces to selectors that
7886
/// aren't type or universal selectors.
7987
const SKIP_DEFAULT_NAMESPACE = 1 << 0;
@@ -112,6 +120,8 @@ bitflags! {
112120

113121
/// Whether we have seen a nesting selector.
114122
const AFTER_NESTING = 1 << 7;
123+
124+
const AFTER_WEBKIT_SCROLLBAR = 1 << 8;
115125
}
116126
}
117127

@@ -168,6 +178,9 @@ pub enum SelectorParseErrorKind<'i> {
168178
NonPseudoElementAfterSlotted,
169179
InvalidPseudoElementAfterSlotted,
170180
InvalidPseudoElementInsideWhere,
181+
InvalidPseudoClassBeforeWebKitScrollbar,
182+
InvalidPseudoClassAfterWebKitScrollbar,
183+
InvalidPseudoClassAfterPseudoElement,
171184
InvalidState,
172185
MissingNestingSelector,
173186
MissingNestingPrefix,
@@ -2354,6 +2367,9 @@ where
23542367
if !p.accepts_state_pseudo_classes() {
23552368
state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT);
23562369
}
2370+
if p.is_webkit_scrollbar() {
2371+
state.insert(SelectorParsingState::AFTER_WEBKIT_SCROLLBAR);
2372+
}
23572373
builder.push_combinator(Combinator::PseudoElement);
23582374
builder.push_simple_selector(Component::PseudoElement(p));
23592375
},
@@ -2651,10 +2667,16 @@ where
26512667
}
26522668

26532669
let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?;
2654-
if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) &&
2655-
!pseudo_class.is_user_action_state()
2656-
{
2657-
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState));
2670+
if state.intersects(SelectorParsingState::AFTER_WEBKIT_SCROLLBAR) {
2671+
if !pseudo_class.is_webkit_scrollbar_state() {
2672+
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterWebKitScrollbar));
2673+
}
2674+
} else if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) {
2675+
if !pseudo_class.is_user_action_state() {
2676+
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement));
2677+
}
2678+
} else if pseudo_class.is_webkit_scrollbar_state() {
2679+
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassBeforeWebKitScrollbar));
26582680
}
26592681
Ok(Component::NonTSPseudoClass(pseudo_class))
26602682
}

src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ pub enum SelectorError<'i> {
114114
NonPseudoElementAfterSlotted,
115115
InvalidPseudoElementAfterSlotted,
116116
InvalidPseudoElementInsideWhere,
117+
InvalidPseudoClassBeforeWebKitScrollbar,
118+
InvalidPseudoClassAfterWebKitScrollbar,
119+
InvalidPseudoClassAfterPseudoElement,
117120
InvalidState,
118121
MissingNestingSelector,
119122
MissingNestingPrefix,
@@ -141,6 +144,9 @@ impl<'i> From<SelectorParseErrorKind<'i>> for SelectorError<'i> {
141144
SelectorParseErrorKind::NonPseudoElementAfterSlotted => SelectorError::NonPseudoElementAfterSlotted,
142145
SelectorParseErrorKind::InvalidPseudoElementAfterSlotted => SelectorError::InvalidPseudoElementAfterSlotted,
143146
SelectorParseErrorKind::InvalidPseudoElementInsideWhere => SelectorError::InvalidPseudoElementInsideWhere,
147+
SelectorParseErrorKind::InvalidPseudoClassBeforeWebKitScrollbar => SelectorError::InvalidPseudoClassBeforeWebKitScrollbar,
148+
SelectorParseErrorKind::InvalidPseudoClassAfterWebKitScrollbar => SelectorError::InvalidPseudoClassAfterWebKitScrollbar,
149+
SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement => SelectorError::InvalidPseudoClassAfterPseudoElement,
144150
SelectorParseErrorKind::InvalidState => SelectorError::InvalidState,
145151
SelectorParseErrorKind::MissingNestingSelector => SelectorError::MissingNestingSelector,
146152
SelectorParseErrorKind::MissingNestingPrefix => SelectorError::MissingNestingPrefix,
@@ -179,6 +185,9 @@ impl<'i> SelectorError<'i> {
179185
InvalidQualNameInAttr(token) => format!("Invalid qualified name in attribute selector: {:?}", token),
180186
ExplicitNamespaceUnexpectedToken(token) => format!("Unexpected token in namespace selector: {:?}", token),
181187
ClassNeedsIdent(token) => format!("Expected identifier in class selector, got {:?}", token),
188+
InvalidPseudoClassBeforeWebKitScrollbar => "Pseudo class must be prefixed by a ::-webkit-scrollbar pseudo element".into(),
189+
InvalidPseudoClassAfterWebKitScrollbar => "Invalid pseudo class after ::-webkit-scrollbar pseudo element".into(),
190+
InvalidPseudoClassAfterPseudoElement => "Invalid pseudo class after pseudo element. Only user action pseudo classes (e.g. :hover, :active) are allowed.".into(),
182191
err => format!("Error parsing selector: {:?}", err)
183192
}
184193
}

src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod context;
2323
#[cfg(test)]
2424
mod tests {
2525
use crate::dependencies::Dependency;
26-
use crate::error::{Error, MinifyErrorKind, ErrorLocation, ParserError, PrinterErrorKind};
26+
use crate::error::{Error, MinifyErrorKind, ErrorLocation, ParserError, PrinterErrorKind, SelectorError};
2727
use crate::properties::custom::Token;
2828
use crate::rules::CssRule;
2929
use crate::stylesheet::*;
@@ -3683,6 +3683,20 @@ mod tests {
36833683
minify_test(".x:has(.bar, #foo) {}", ".x:has(.bar,#foo){}");
36843684
minify_test(".x:has(span + span) {}", ".x:has(span+span){}");
36853685
minify_test("a:has(:visited) {}", "a:has(:visited){}");
3686+
for element in ["-webkit-scrollbar", "-webkit-scrollbar-button", "-webkit-scrollbar-track", "-webkit-scrollbar-track-piece", "-webkit-scrollbar-thumb", "-webkit-scrollbar-corner", "-webkit-resizer"] {
3687+
for class in ["horizontal", "vertical", "decrement", "increment", "start", "end", "double-button", "single-button", "no-button", "corner-present", "window-inactive"] {
3688+
minify_test(&format!("::{}:{} {{}}", element, class), &format!("::{}:{}{{}}", element, class));
3689+
}
3690+
}
3691+
for class in ["horizontal", "vertical", "decrement", "increment", "start", "end", "double-button", "single-button", "no-button", "corner-present", "window-inactive"] {
3692+
error_test(&format!(":{} {{}}", class), ParserError::SelectorError(SelectorError::InvalidPseudoClassBeforeWebKitScrollbar));
3693+
}
3694+
for element in ["-webkit-scrollbar", "-webkit-scrollbar-button", "-webkit-scrollbar-track", "-webkit-scrollbar-track-piece", "-webkit-scrollbar-thumb", "-webkit-scrollbar-corner", "-webkit-resizer"] {
3695+
error_test(&format!("::{}:hover {{}}", element), ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterWebKitScrollbar));
3696+
}
3697+
3698+
error_test("a::first-letter:last-child {}", ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement));
3699+
minify_test("a:last-child::first-letter {}", "a:last-child:first-letter{}");
36863700

36873701
prefix_test(
36883702
".test:not(.foo, .bar) {}",

src/selector.rs

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,19 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'i> {
159159
"-webkit-autofill" => Autofill(VendorPrefix::WebKit),
160160
"-o-autofill" => Autofill(VendorPrefix::O),
161161

162+
// https://webkit.org/blog/363/styling-scrollbars/
163+
"horizontal" => WebKitScrollbar(WebKitScrollbarPseudoClass::Horizontal),
164+
"vertical" => WebKitScrollbar(WebKitScrollbarPseudoClass::Vertical),
165+
"decrement" => WebKitScrollbar(WebKitScrollbarPseudoClass::Decrement),
166+
"increment" => WebKitScrollbar(WebKitScrollbarPseudoClass::Increment),
167+
"start" => WebKitScrollbar(WebKitScrollbarPseudoClass::Start),
168+
"end" => WebKitScrollbar(WebKitScrollbarPseudoClass::End),
169+
"double-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::DoubleButton),
170+
"single-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::SingleButton),
171+
"no-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::NoButton),
172+
"corner-present" => WebKitScrollbar(WebKitScrollbarPseudoClass::CornerPresent),
173+
"window-inactive" => WebKitScrollbar(WebKitScrollbarPseudoClass::WindowInactive),
174+
162175
_ => Custom(name.into())
163176
};
164177

@@ -205,6 +218,15 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'i> {
205218
"file-selector-button" => FileSelectorButton(VendorPrefix::None),
206219
"-webkit-file-upload-button" => FileSelectorButton(VendorPrefix::WebKit),
207220
"-ms-browse" => FileSelectorButton(VendorPrefix::Ms),
221+
222+
"-webkit-scrollbar" => WebKitScrollbar(WebKitScrollbarPseudoElement::Scrollbar),
223+
"-webkit-scrollbar-button" => WebKitScrollbar(WebKitScrollbarPseudoElement::Button),
224+
"-webkit-scrollbar-track" => WebKitScrollbar(WebKitScrollbarPseudoElement::Track),
225+
"-webkit-scrollbar-track-piece" => WebKitScrollbar(WebKitScrollbarPseudoElement::TrackPiece),
226+
"-webkit-scrollbar-thumb" => WebKitScrollbar(WebKitScrollbarPseudoElement::Thumb),
227+
"-webkit-scrollbar-corner" => WebKitScrollbar(WebKitScrollbarPseudoElement::Corner),
228+
"-webkit-resizer" => WebKitScrollbar(WebKitScrollbarPseudoElement::Resizer),
229+
208230
_ => Custom(name.into())
209231
};
210232

@@ -313,18 +335,41 @@ pub enum PseudoClass<'i> {
313335
Local(Box<parcel_selectors::parser::Selector<'i, Selectors>>),
314336
Global(Box<parcel_selectors::parser::Selector<'i, Selectors>>),
315337

338+
// https://webkit.org/blog/363/styling-scrollbars/
339+
WebKitScrollbar(WebKitScrollbarPseudoClass),
340+
316341
Custom(CowArcStr<'i>)
317342
}
318343

344+
/// https://webkit.org/blog/363/styling-scrollbars/
345+
#[derive(Clone, Eq, PartialEq)]
346+
pub enum WebKitScrollbarPseudoClass {
347+
Horizontal,
348+
Vertical,
349+
Decrement,
350+
Increment,
351+
Start,
352+
End,
353+
DoubleButton,
354+
SingleButton,
355+
NoButton,
356+
CornerPresent,
357+
WindowInactive
358+
}
359+
319360
impl<'i> parcel_selectors::parser::NonTSPseudoClass<'i> for PseudoClass<'i> {
320361
type Impl = Selectors;
321362

322363
fn is_active_or_hover(&self) -> bool {
323-
matches!(*self, PseudoClass::Active | PseudoClass::Hover)
364+
matches!(*self, PseudoClass::Active | PseudoClass::Hover)
324365
}
325366

326367
fn is_user_action_state(&self) -> bool {
327-
matches!(*self, PseudoClass::Active | PseudoClass::Hover | PseudoClass::Focus)
368+
matches!(*self, PseudoClass::Active | PseudoClass::Hover | PseudoClass::Focus)
369+
}
370+
371+
fn is_webkit_scrollbar_state(&self) -> bool {
372+
matches!(*self, PseudoClass::WebKitScrollbar(..))
328373
}
329374
}
330375

@@ -461,6 +506,24 @@ impl<'a, 'i> ToCssWithContext<'a, 'i> for PseudoClass<'i> {
461506
Ok(())
462507
},
463508

509+
// https://webkit.org/blog/363/styling-scrollbars/
510+
WebKitScrollbar(s) => {
511+
use WebKitScrollbarPseudoClass::*;
512+
dest.write_str(match s {
513+
Horizontal => ":horizontal",
514+
Vertical => ":vertical",
515+
Decrement => ":decrement",
516+
Increment => ":increment",
517+
Start => ":start",
518+
End => ":end",
519+
DoubleButton => ":double-button",
520+
SingleButton => ":single-button",
521+
NoButton => ":no-button",
522+
CornerPresent => ":corner-present",
523+
WindowInactive => ":window-inactive"
524+
})
525+
},
526+
464527
Lang(_) | Dir(_) => unreachable!(),
465528
Custom(val) => {
466529
dest.write_char(':')?;
@@ -526,9 +589,28 @@ pub enum PseudoElement<'i> {
526589
Marker,
527590
Backdrop(VendorPrefix),
528591
FileSelectorButton(VendorPrefix),
592+
WebKitScrollbar(WebKitScrollbarPseudoElement),
529593
Custom(CowArcStr<'i>)
530594
}
531595

596+
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
597+
pub enum WebKitScrollbarPseudoElement {
598+
/// ::-webkit-scrollbar
599+
Scrollbar,
600+
/// ::-webkit-scrollbar-button
601+
Button,
602+
/// ::-webkit-scrollbar-track
603+
Track,
604+
/// ::-webkit-scrollbar-track-piece
605+
TrackPiece,
606+
/// ::-webkit-scrollbar-thumb
607+
Thumb,
608+
/// ::-webkit-scrollbar-corner
609+
Corner,
610+
/// ::-webkit-resizer
611+
Resizer
612+
}
613+
532614
impl<'i> cssparser::ToCss for PseudoElement<'i> {
533615
fn to_css<W>(&self, _: &mut W)-> std::fmt::Result where W: fmt::Write {
534616
unreachable!();
@@ -589,6 +671,18 @@ impl<'i> ToCss for PseudoElement<'i> {
589671
dest.write_str("file-selector-button")
590672
}
591673
}
674+
WebKitScrollbar(s) => {
675+
use WebKitScrollbarPseudoElement::*;
676+
dest.write_str(match s {
677+
Scrollbar => "::-webkit-scrollbar",
678+
Button => "::-webkit-scrollbar-button",
679+
Track => "::-webkit-scrollbar-track",
680+
TrackPiece => "::-webkit-scrollbar-track-piece",
681+
Thumb => "::-webkit-scrollbar-thumb",
682+
Corner => "::-webkit-scrollbar-corner",
683+
Resizer => "::-webkit-resizer"
684+
})
685+
}
592686
Custom(val) => {
593687
dest.write_str("::")?;
594688
return dest.write_str(val)
@@ -618,6 +712,10 @@ impl<'i> parcel_selectors::parser::PseudoElement<'i> for PseudoElement<'i> {
618712
PseudoElement::FileSelectorButton(_)
619713
)
620714
}
715+
716+
fn is_webkit_scrollbar(&self) -> bool {
717+
matches!(*self, PseudoElement::WebKitScrollbar(..))
718+
}
621719
}
622720

623721
impl<'i> PseudoElement<'i> {

0 commit comments

Comments
 (0)