Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/build-prefixes.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ let mdnFeatures = {
picker: mdn.css.selectors.picker.__compat.support,
pickerIcon: mdn.css.selectors['picker-icon'].__compat.support,
checkmark: mdn.css.selectors.checkmark.__compat.support,
scrollMarker: mdn.css.selectors['scroll-marker'].__compat.support,
scrollMarkerGroup: mdn.css.selectors['scroll-marker-group'].__compat.support,
scrollButton: mdn.css.selectors['scroll-button'].__compat.support,
grammarError: mdn.css.selectors['grammar-error'].__compat.support,
spellingError: mdn.css.selectors['spelling-error'].__compat.support,
statePseudoClass: Object.fromEntries(
Expand Down
24 changes: 24 additions & 0 deletions selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub trait PseudoElement<'i>: Sized + ToCss {
false
}

fn is_scroll_marker(&self) -> bool {
false
}

fn is_unknown(&self) -> bool {
false
}
Expand All @@ -71,6 +75,18 @@ pub trait NonTSPseudoClass<'i>: Sized + ToCss {
false
}

/// Whether this pseudo-class may follow a `::scroll-marker` pseudo-element.
///
/// Selectors 4 allows only the user action pseudo-classes after a
/// pseudo-element. css-overflow-5 extends that for scroll markers: it
/// defines `:target-current`, `:target-before` and `:target-after`
/// specifically to match them.
///
/// https://drafts.csswg.org/css-overflow-5/#selectordef-target-current
fn is_valid_after_scroll_marker(&self) -> bool {
self.is_user_action_state()
}

fn visit<V>(&self, _visitor: &mut V) -> bool
where
V: SelectorVisitor<'i, Impl = Self::Impl>,
Expand Down Expand Up @@ -137,6 +153,7 @@ bitflags! {
const AFTER_WEBKIT_SCROLLBAR = 1 << 8;
const AFTER_VIEW_TRANSITION = 1 << 9;
const AFTER_UNKNOWN_PSEUDO_ELEMENT = 1 << 10;
const AFTER_SCROLL_MARKER = 1 << 11;
}
}

Expand Down Expand Up @@ -2791,6 +2808,9 @@ where
if p.is_view_transition() {
state.insert(SelectorParsingState::AFTER_VIEW_TRANSITION);
}
if p.is_scroll_marker() {
state.insert(SelectorParsingState::AFTER_SCROLL_MARKER);
}
builder.push_simple_selector(Component::PseudoElement(p));
}
}
Expand Down Expand Up @@ -3126,6 +3146,10 @@ where
if !pseudo_class.is_valid_after_webkit_scrollbar() {
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterWebKitScrollbar));
}
} else if state.intersects(SelectorParsingState::AFTER_SCROLL_MARKER) {
if !pseudo_class.is_valid_after_scroll_marker() {
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement));
}
} else if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) {
if !pseudo_class.is_user_action_state() {
return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement));
Expand Down
37 changes: 37 additions & 0 deletions src/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ pub enum Feature {
RicUnit,
RlhUnit,
RoundFunction,
ScrollButton,
ScrollMarker,
ScrollMarkerGroup,
SearchText,
Selection,
Selectors2,
Expand Down Expand Up @@ -3789,6 +3792,40 @@ impl Feature {
return false;
}
}
Feature::ScrollMarker | Feature::ScrollMarkerGroup | Feature::ScrollButton => {
if let Some(version) = browsers.chrome {
if version < 8847360 {
return false;
}
}
if let Some(version) = browsers.edge {
if version < 8847360 {
return false;
}
}
if let Some(version) = browsers.opera {
if version < 5832704 {
return false;
}
}
if let Some(version) = browsers.samsung {
if version < 1900544 {
return false;
}
}
if let Some(version) = browsers.android {
if version < 8847360 {
return false;
}
}
if browsers.firefox.is_some()
|| browsers.ie.is_some()
|| browsers.ios_saf.is_some()
|| browsers.safari.is_some()
{
return false;
}
}
Feature::GrammarError | Feature::SpellingError => {
if let Some(version) = browsers.chrome {
if version < 7929856 {
Expand Down
57 changes: 57 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6790,6 +6790,63 @@ mod tests {
}),
);

// Scroll navigation control pseudo-elements, from the same spec section.
minify_test_with_options(
"li::scroll-marker { content: \"\" }",
"li::scroll-marker{content:\"\"}",
scroll_navigation_controls_options.clone(),
);
minify_test_with_options(
".carousel::scroll-marker-group { display: flex }",
".carousel::scroll-marker-group{display:flex}",
scroll_navigation_controls_options.clone(),
);
minify_test_with_options(
".carousel::scroll-button(right) { content: \">\" }",
".carousel::scroll-button(right){content:\">\"}",
scroll_navigation_controls_options.clone(),
);
minify_test_with_options(
".carousel::scroll-button(*) { color: green }",
".carousel::scroll-button(*){color:green}",
scroll_navigation_controls_options.clone(),
);
minify_test_with_options(
".carousel::scroll-button(inline-end) { color: green }",
".carousel::scroll-button(inline-end){color:green}",
scroll_navigation_controls_options.clone(),
);
// The keyword is matched case-insensitively and serialized canonically.
minify_test_with_options(
".carousel::SCROLL-BUTTON(BLOCK-START) { color: green }",
".carousel::scroll-button(block-start){color:green}",
scroll_navigation_controls_options.clone(),
);
// The shape the spec is actually for: a marker styled by its scroll position.
minify_test_with_options(
"li::scroll-marker:target-current { color: green }",
"li::scroll-marker:target-current{color:green}",
scroll_navigation_controls_options.clone(),
);
// ...which stays limited to scroll markers.
error_test_with_options(
"a::before:target-current { color: green }",
ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement),
scroll_navigation_controls_options.clone(),
);
// An unknown direction is not a scroll button.
error_test_with_options(
".carousel::scroll-button(sideways) { color: green }",
ParserError::SelectorError(SelectorError::UnexpectedIdent("sideways".into())),
scroll_navigation_controls_options.clone(),
);
// Without the flag these are unknown pseudo-elements, and still pass through.
minify_test("li::scroll-marker { color: green }", "li::scroll-marker{color:green}");
minify_test(
".carousel::scroll-button(right) { color: green }",
".carousel::scroll-button(right){color:green}",
);

error_test_with_options(
"a::before:target-current { color: green }",
ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement),
Expand Down
136 changes: 136 additions & 0 deletions src/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'i> {
name: CowRcStr<'i>,
) -> Result<PseudoElement<'i>, ParseError<'i, Self::Error>> {
use PseudoElement::*;
let scroll_navigation_controls = self
.options
.flags
.contains(ParserFlags::SCROLL_NAVIGATION_CONTROLS);
let pseudo_element = match_ignore_ascii_case! { &name,
"before" => Before,
"after" => After,
Expand Down Expand Up @@ -310,6 +314,10 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'i> {
"picker-icon" => PickerIcon,
"checkmark" => Checkmark,

// https://drafts.csswg.org/css-overflow-5/#scroll-navigation-controls
"scroll-marker" if scroll_navigation_controls => ScrollMarker,
"scroll-marker-group" if scroll_navigation_controls => ScrollMarkerGroup,

"view-transition" => ViewTransition,

"grammar-error" => GrammarError,
Expand All @@ -332,11 +340,17 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'i> {
arguments: &mut Parser<'i, 't>,
) -> Result<<Self::Impl as SelectorImpl<'i>>::PseudoElement, ParseError<'i, Self::Error>> {
use PseudoElement::*;
let scroll_navigation_controls = self
.options
.flags
.contains(ParserFlags::SCROLL_NAVIGATION_CONTROLS);
let pseudo_element = match_ignore_ascii_case! { &name,
"cue" => CueFunction { selector: Box::new(Selector::parse(self, arguments)?) },
"cue-region" => CueRegionFunction { selector: Box::new(Selector::parse(self, arguments)?) },
"highlight" => HighlightFunction { name: CustomIdent::parse(arguments)? },
"picker" => PickerFunction { identifier: Ident::parse(arguments)? },
// https://drafts.csswg.org/css-overflow-5/#scroll-navigation-controls
"scroll-button" if scroll_navigation_controls => ScrollButton { direction: ScrollButtonDirection::parse(arguments)? },
"view-transition-group" => ViewTransitionGroup { part: ViewTransitionPartSelector::parse(arguments)? },
"view-transition-image-pair" => ViewTransitionImagePair { part: ViewTransitionPartSelector::parse(arguments)? },
"view-transition-old" => ViewTransitionOld { part: ViewTransitionPartSelector::parse(arguments)? },
Expand Down Expand Up @@ -649,6 +663,16 @@ impl<'i> parcel_selectors::parser::NonTSPseudoClass<'i> for PseudoClass<'i> {
)
}

fn is_valid_after_scroll_marker(&self) -> bool {
// css-overflow-5 defines these three to match ::scroll-marker, so they are
// valid there even though they are not user action states.
self.is_user_action_state()
|| matches!(
*self,
PseudoClass::TargetCurrent | PseudoClass::TargetBefore | PseudoClass::TargetAfter
)
}

fn is_valid_before_webkit_scrollbar(&self) -> bool {
!matches!(*self, PseudoClass::WebKitScrollbar(..))
}
Expand Down Expand Up @@ -1017,6 +1041,15 @@ pub enum PseudoElement<'i> {
PickerIcon,
/// The [::checkmark](https://drafts.csswg.org/css-forms-1/#styling-checkmarks-the-checkmark-pseudo-element) pseudo element.
Checkmark,
/// The [::scroll-marker](https://drafts.csswg.org/css-overflow-5/#selectordef-scroll-marker) pseudo element.
ScrollMarker,
/// The [::scroll-marker-group](https://drafts.csswg.org/css-overflow-5/#selectordef-scroll-marker-group) pseudo element.
ScrollMarkerGroup,
/// The [::scroll-button()](https://drafts.csswg.org/css-overflow-5/#selectordef-scroll-button---scroll-button-direction) functional pseudo element.
ScrollButton {
/// The direction the button scrolls its scroll container toward.
direction: ScrollButtonDirection,
},
/// The [::grammar-error](https://drafts.csswg.org/css-pseudo/#selectordef-grammar-error) pseudo element.
GrammarError,
/// The [::spelling-error](https://drafts.csswg.org/css-pseudo/#selectordef-spelling-error) pseudo element.
Expand Down Expand Up @@ -1298,6 +1331,13 @@ where
}
PickerIcon => dest.write_str("::picker-icon"),
Checkmark => dest.write_str("::checkmark"),
ScrollMarker => dest.write_str("::scroll-marker"),
ScrollMarkerGroup => dest.write_str("::scroll-marker-group"),
ScrollButton { direction } => {
dest.write_str("::scroll-button(")?;
direction.to_css(dest)?;
dest.write_char(')')
}
GrammarError => dest.write_str("::grammar-error"),
SpellingError => dest.write_str("::spelling-error"),
Custom { name: val } => {
Expand All @@ -1314,6 +1354,95 @@ where
}
}

/// A [`<scroll-button-direction>`](https://drafts.csswg.org/css-overflow-5/#typedef-scroll-button-direction)
/// value, or `*`, as accepted by the `::scroll-button()` pseudo element.
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum ScrollButtonDirection {
/// `*`, which selects every scroll button of the scroll container.
#[cfg_attr(feature = "serde", serde(rename = "*"))]
All,
/// `up`
Up,
/// `down`
Down,
/// `left`
Left,
/// `right`
Right,
/// `block-start`
BlockStart,
/// `block-end`
BlockEnd,
/// `inline-start`
InlineStart,
/// `inline-end`
InlineEnd,
/// `prev`
Prev,
/// `next`
Next,
}

impl ScrollButtonDirection {
/// Returns a string representation of the value.
pub fn as_str(&self) -> &str {
use ScrollButtonDirection::*;
match self {
All => "*",
Up => "up",
Down => "down",
Left => "left",
Right => "right",
BlockStart => "block-start",
BlockEnd => "block-end",
InlineStart => "inline-start",
InlineEnd => "inline-end",
Prev => "prev",
Next => "next",
}
}
}

impl<'i> Parse<'i> for ScrollButtonDirection {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if input.try_parse(|input| input.expect_delim('*')).is_ok() {
return Ok(ScrollButtonDirection::All);
}

let location = input.current_source_location();
let ident = input.expect_ident()?;
match_ignore_ascii_case! { &*ident,
"up" => Ok(ScrollButtonDirection::Up),
"down" => Ok(ScrollButtonDirection::Down),
"left" => Ok(ScrollButtonDirection::Left),
"right" => Ok(ScrollButtonDirection::Right),
"block-start" => Ok(ScrollButtonDirection::BlockStart),
"block-end" => Ok(ScrollButtonDirection::BlockEnd),
"inline-start" => Ok(ScrollButtonDirection::InlineStart),
"inline-end" => Ok(ScrollButtonDirection::InlineEnd),
"prev" => Ok(ScrollButtonDirection::Prev),
"next" => Ok(ScrollButtonDirection::Next),
_ => Err(location.new_custom_error(ParserError::SelectorError(SelectorError::UnexpectedIdent(ident.into()))))
}
}
}

impl ToCss for ScrollButtonDirection {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
dest.write_str(self.as_str())
}
}

impl<'i> parcel_selectors::parser::PseudoElement<'i> for PseudoElement<'i> {
type Impl = Selectors;

Expand Down Expand Up @@ -1350,6 +1479,10 @@ impl<'i> parcel_selectors::parser::PseudoElement<'i> for PseudoElement<'i> {
)
}

fn is_scroll_marker(&self) -> bool {
matches!(*self, PseudoElement::ScrollMarker)
}

fn is_unknown(&self) -> bool {
matches!(
*self,
Expand Down Expand Up @@ -2021,6 +2154,9 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool {
PseudoElement::PickerFunction { identifier: _ } => Feature::Picker,
PseudoElement::PickerIcon => Feature::PickerIcon,
PseudoElement::Checkmark => Feature::Checkmark,
PseudoElement::ScrollMarker => Feature::ScrollMarker,
PseudoElement::ScrollMarkerGroup => Feature::ScrollMarkerGroup,
PseudoElement::ScrollButton { direction: _ } => Feature::ScrollButton,
PseudoElement::GrammarError => Feature::GrammarError,
PseudoElement::SpellingError => Feature::SpellingError,
PseudoElement::Custom { name: _ } | _ => return false,
Expand Down