Skip to content
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ A CSS parser, transformer, and minifier written in Rust.
- `hwb()` color syntax
- Percent syntax for opacity
- `#rgba` and `#rrggbbaa` hex colors
- Selectors
- `:not` with multiple arguments
- `:lang` with multiple arguments
- `:dir`
- `:is`
- Double position gradient stops (e.g. `red 40% 80%`)
- `clamp()` function
- Alignment shorthands (e.g. `place-items`)
Expand Down
46 changes: 45 additions & 1 deletion scripts/build-prefixes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ prefixes['clip-path'].browsers = prefixes['clip-path'].browsers.filter(b => {
);
});

prefixes['any-pseudo'] = {
browsers: Object.entries(mdn.css.selectors.is.__compat.support)
.flatMap(([key, value]) => {
if (Array.isArray(value)) {
key = MDN_BROWSER_MAPPING[key] || key;
let any = value.find(v => v.alternative_name?.includes('-any'))?.version_added;
let supported = value.find(x => x.version_added && !x.alternative_name)?.version_added;
if (any && supported) {
let parts = supported.split('.');
parts[0]--;
supported = parts.join('.');
return [`${key} ${any}}`, `${key} ${supported}`];
}
}

return [];
})
}

console.log(prefixes['any-pseudo'])

let flexSpec = {};
let oldGradient = {};
let p = new Map();
Expand Down Expand Up @@ -199,7 +220,23 @@ let mdnFeatures = {
logicalTextAlign: mdn.css.properties['text-align']['flow_relative_values_start_and_end'].__compat.support,
labColors: mdn.css.types.color.lab.__compat.support,
oklabColors: {},
colorFunction: mdn.css.types.color.color.__compat.support
colorFunction: mdn.css.types.color.color.__compat.support,
anyPseudo: Object.fromEntries(
Object.entries(mdn.css.selectors.is.__compat.support)
.map(([key, value]) => {
if (Array.isArray(value)) {
value = value
.filter(v => v.alternative_name?.includes('-any'))
.map(({alternative_name, ...other}) => other);
}

if (value && value.length) {
return [key, value];
} else {
return [key, {version_added: false}];
}
})
)
};

for (let feature in mdnFeatures) {
Expand Down Expand Up @@ -239,6 +276,12 @@ addValue(compat, {
ios_saf: parseVersion('10.3')
}, 'p3Colors');

addValue(compat, {
// https://github.com/WebKit/WebKit/commit/baed0d8b0abf366e1d9a6105dc378c59a5f21575
safari: parseVersion('10.1'),
ios_saf: parseVersion('10.3')
}, 'langList');

let prefixMapping = {
webkit: 'WebKit',
moz: 'Moz',
Expand Down Expand Up @@ -376,6 +419,7 @@ fs.writeFileSync('src/compat.rs', c);


function parseVersion(version) {
version = version.replace('≤', '');
let [major, minor = '0', patch = '0'] = version
.split('-')[0]
.split('.')
Expand Down
2 changes: 1 addition & 1 deletion selectors/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ where
| Component::NonTSPseudoClass(..) => {
specificity.class_like_selectors += 1;
}
Component::Negation(ref list) | Component::Is(ref list) => {
Component::Negation(ref list) | Component::Is(ref list) | Component::Any(_, ref list) => {
// https://drafts.csswg.org/selectors/#specificity-rules:
//
// The specificity of an :is() pseudo-class is replaced by the
Expand Down
16 changes: 9 additions & 7 deletions selectors/matching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -795,14 +795,16 @@ where
matches_generic_nth_child(element, context, 0, 1, true, false, flags_setter)
&& matches_generic_nth_child(element, context, 0, 1, true, true, flags_setter)
}
Component::Is(ref list) | Component::Where(ref list) => context.shared.nest(|context| {
for selector in &**list {
if matches_complex_selector(selector.iter(), element, context, flags_setter) {
return true;
Component::Is(ref list) | Component::Where(ref list) | Component::Any(_, ref list) => {
context.shared.nest(|context| {
for selector in &**list {
if matches_complex_selector(selector.iter(), element, context, flags_setter) {
return true;
}
}
}
false
}),
false
})
}
Component::Negation(ref list) => context.shared.nest_for_negation(|context| {
for selector in &**list {
if matches_complex_selector(selector.iter(), element, context, flags_setter) {
Expand Down
49 changes: 44 additions & 5 deletions selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ macro_rules! with_all_bounds {
/// non tree-structural pseudo-classes
/// (see: https://drafts.csswg.org/selectors/#structural-pseudos)
type NonTSPseudoClass: $($CommonBounds)* + NonTSPseudoClass<'i, Impl = Self>;
type VendorPrefix: Sized + Eq + Clone + ToCss;

/// pseudo-elements
type PseudoElement: $($CommonBounds)* + PseudoElement<'i, Impl = Self>;
Expand Down Expand Up @@ -274,8 +275,8 @@ pub trait Parser<'i> {
}

/// Whether the given function name is an alias for the `:is()` function.
fn is_is_alias(&self, _name: &str) -> bool {
false
fn parse_any_prefix(&self, _name: &str) -> Option<<Self::Impl as SelectorImpl<'i>>::VendorPrefix> {
None
}

/// Whether to parse the `:host` pseudo-class.
Expand Down Expand Up @@ -640,6 +641,16 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> {
self.0.is_part()
}

#[inline]
pub fn append(&mut self, component: Component<'i, Impl>) {
let index = self
.1
.iter()
.position(|c| matches!(*c, Component::Combinator(..) | Component::PseudoElement(..)))
.unwrap_or(self.1.len());
self.1.insert(index, component);
}

#[inline]
pub fn parts(&self) -> Option<&[Impl::Identifier]> {
if !self.is_part() {
Expand Down Expand Up @@ -697,6 +708,13 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> {
})
}

#[inline]
pub fn has_combinator(&self) -> bool {
self
.iter_raw_match_order()
.any(|c| matches!(*c, Component::Combinator(combinator) if combinator.is_tree_combinator()))
}

/// Returns an iterator over this selector in matching order (right-to-left).
/// When a combinator is reached, the iterator will return None, and
/// next_sequence() may be called to continue to the next sequence.
Expand Down Expand Up @@ -759,6 +777,11 @@ impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> {
self.1.iter()
}

#[inline]
pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut<Component<'i, Impl>> {
self.1.iter_mut()
}

/// Returns the combinator at index `index` (zero-indexed from the left),
/// or panics if the component is not a combinator.
#[inline]
Expand Down Expand Up @@ -1032,6 +1055,14 @@ impl Combinator {
pub fn is_sibling(&self) -> bool {
matches!(*self, Combinator::NextSibling | Combinator::LaterSibling)
}

#[inline]
pub fn is_tree_combinator(&self) -> bool {
matches!(
*self,
Combinator::Child | Combinator::Descendant | Combinator::NextSibling | Combinator::LaterSibling
)
}
}

/// A CSS simple selector or combinator. We store both in the same enum for
Expand Down Expand Up @@ -1122,6 +1153,7 @@ pub enum Component<'i, Impl: SelectorImpl<'i>> {
///
/// Same comment as above re. the argument.
Is(Box<[Selector<'i, Impl>]>),
Any(Impl::VendorPrefix, Box<[Selector<'i, Impl>]>),
/// The `:has` pseudo-class.
///
/// https://www.w3.org/TR/selectors/#relational
Expand Down Expand Up @@ -1585,12 +1617,17 @@ impl<'i, Impl: SelectorImpl<'i>> ToCss for Component<'i, Impl> {
write_affine(dest, a, b)?;
dest.write_char(')')
}
Is(ref list) | Where(ref list) | Negation(ref list) | Has(ref list) => {
Is(ref list) | Where(ref list) | Negation(ref list) | Has(ref list) | Any(_, ref list) => {
match *self {
Where(..) => dest.write_str(":where(")?,
Is(..) => dest.write_str(":is(")?,
Negation(..) => dest.write_str(":not(")?,
Has(..) => dest.write_str(":has(")?,
Any(ref prefix, _) => {
dest.write_char(':')?;
prefix.to_css(dest)?;
dest.write_str("any(")?;
}
_ => unreachable!(),
}
serialize_selector_list(list.iter(), dest)?;
Expand Down Expand Up @@ -2333,6 +2370,7 @@ where
}
Ok(Component::Has(inner.0.into_vec().into_boxed_slice()))
}

fn parse_functional_pseudo_class<'i, 't, P, Impl>(
parser: &P,
input: &mut CssParser<'i, 't>,
Expand Down Expand Up @@ -2363,8 +2401,8 @@ where
_ => {}
}

if parser.parse_is_and_where() && parser.is_is_alias(&name) {
return parse_is_or_where(parser, input, state, Component::Is);
if let Some(prefix) = parser.parse_any_prefix(&name) {
return parse_is_or_where(parser, input, state, |selectors| Component::Any(prefix, selectors));
}

if !state.allows_custom_functional_pseudo_classes() {
Expand Down Expand Up @@ -2679,6 +2717,7 @@ pub mod tests {
type BorrowedNamespaceUrl = DummyAtom;
type NonTSPseudoClass = PseudoClass;
type PseudoElement = PseudoElement;
type VendorPrefix = u8;
}

#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
Expand Down
49 changes: 48 additions & 1 deletion src/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::targets::Browsers;

#[derive(Clone, Copy, PartialEq)]
pub enum Feature {
AnyPseudo,
Clamp,
ColorFunction,
CssAnyLink,
Expand Down Expand Up @@ -38,6 +39,7 @@ pub enum Feature {
FormValidation,
Fullscreen,
LabColors,
LangList,
LogicalBorderRadius,
LogicalBorders,
LogicalInset,
Expand Down Expand Up @@ -1745,7 +1747,52 @@ impl Feature {
return false;
}
}
Feature::P3Colors => {
Feature::AnyPseudo => {
if let Some(version) = browsers.chrome {
if version < 1179648 {
return false;
}
}
if let Some(version) = browsers.edge {
if version < 5177344 {
return false;
}
}
if let Some(version) = browsers.firefox {
if version < 262144 {
return false;
}
}
if let Some(version) = browsers.opera {
if version < 917504 {
return false;
}
}
if let Some(version) = browsers.safari {
if version < 327680 {
return false;
}
}
if let Some(version) = browsers.ios_saf {
if version < 327680 {
return false;
}
}
if let Some(version) = browsers.samsung {
if version < 65536 {
return false;
}
}
if let Some(version) = browsers.android {
if version < 2424832 {
return false;
}
}
if browsers.ie.is_some() {
return false;
}
}
Feature::P3Colors | Feature::LangList => {
if let Some(version) = browsers.safari {
if version < 655616 {
return false;
Expand Down
Loading