Skip to content
157 changes: 114 additions & 43 deletions src/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o,
) -> Result<PseudoClass<'i>, ParseError<'i, Self::Error>> {
use PseudoClass::*;
let pseudo_class = match_ignore_ascii_case! { &name,
// CSS Modules non-functional aliases: :local and :global without arguments
// Treat them as if they were functional with a nesting selector argument, i.e.
// :local(&) and :global(&), so they affect nested rules blocks like `:global { ... }`.
"local" if self.options.css_modules.is_some() => {
Local { selector: Box::new(Selector::from(Component::Nesting)) }
},
"global" if self.options.css_modules.is_some() => {
dbg!("global");
Global { selector: Box::new(Selector::from(Component::Nesting)) }
},
// https://drafts.csswg.org/selectors-4/#useraction-pseudos
"hover" => Hover,
"active" => Active,
Expand Down Expand Up @@ -193,11 +203,6 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o,
"no-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::NoButton),
"corner-present" => WebKitScrollbar(WebKitScrollbarPseudoClass::CornerPresent),
"window-inactive" => WebKitScrollbar(WebKitScrollbarPseudoClass::WindowInactive),

"local" | "global" if self.options.css_modules.is_some() => {
return Err(loc.new_custom_error(SelectorParseErrorKind::AmbiguousCssModuleClass(name.clone())))
},

_ => {
if !name.starts_with('-') {
self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone())));
Expand Down Expand Up @@ -799,9 +804,11 @@ where

Local { selector } => serialize_selector(selector, dest, context, false),
Global { selector } => {
dest.write_str(":global(")?;
let css_module = std::mem::take(&mut dest.css_module);
serialize_selector(selector, dest, context, false)?;
serialize_selector_with_css_modules(selector, dest, context, false, false)?;
dest.css_module = css_module;
dest.write_char(')')?;
Ok(())
}

Expand Down Expand Up @@ -1376,11 +1383,12 @@ impl<'a, 'i> ToCss for Selector<'i> {
}
}

fn serialize_selector<'a, 'i, W>(
fn serialize_selector_with_css_modules<'a, 'i, W>(
selector: &Selector<'i>,
dest: &mut Printer<W>,
context: Option<&StyleContext>,
mut is_relative: bool,
mut handle_css_modules: bool,
) -> Result<(), PrinterError>
where
W: fmt::Write,
Expand All @@ -1403,9 +1411,7 @@ where
let should_compile_nesting = should_compile!(dest.targets.current, Nesting);

let mut first = true;
let mut combinators_exhausted = false;
for mut compound in compound_selectors {
debug_assert!(!combinators_exhausted);

// Skip implicit :scope in relative selectors (e.g. :has(:scope > foo) -> :has(> foo))
if is_relative && matches!(compound.get(0), Some(Component::Scope)) {
Expand Down Expand Up @@ -1465,7 +1471,7 @@ where
}

for simple in iter {
serialize_component(simple, dest, context)?;
serialize_component_with_css_modules(simple, dest, context, &mut handle_css_modules)?;
}

if swap_nesting {
Expand All @@ -1492,62 +1498,65 @@ where
let mut iter = compound.iter();
if has_leading_nesting && should_compile_nesting && is_type_selector(compound.get(first_non_namespace)) {
// Swap nesting and type selector (e.g. &div -> div&).
// This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not).
let nesting = iter.next().unwrap();
let local = iter.next().unwrap();
serialize_component(local, dest, context)?;
serialize_component_with_css_modules(local, dest, context, &mut handle_css_modules)?;

// Also check the next item in case of namespaces.
if first_non_namespace > first_index {
let local = iter.next().unwrap();
serialize_component(local, dest, context)?;
serialize_component_with_css_modules(local, dest, context, &mut handle_css_modules)?;
}

serialize_component(nesting, dest, context)?;
serialize_component_with_css_modules(nesting, dest, context, &mut handle_css_modules)?;
} else if has_leading_nesting && should_compile_nesting {
// Nesting selector may serialize differently if it is leading, due to type selectors.
iter.next();
serialize_nesting(dest, context, true)?;
}

for simple in iter {
if let Component::ExplicitUniversalType = *simple {
// Can't have a namespace followed by a pseudo-element
// selector followed by a universal selector in the same
// compound selector, so we don't have to worry about the
// real namespace being in a different `compound`.
if can_elide_namespace {
// Skip namespace selectors if we can elide them.
if can_elide_namespace && first_non_namespace > first_index {
if let Component::DefaultNamespace(..) = simple {
continue;
}
}
serialize_component(simple, dest, context)?;
serialize_component_with_css_modules(simple, dest, context, &mut handle_css_modules)?;
}
}

// 3. If this is not the last part of the chain of the selector
// append a single SPACE (U+0020), followed by the combinator
// ">", "+", "~", ">>", "||", as appropriate, followed by another
// single SPACE (U+0020) if the combinator was not whitespace, to
// s.
match next_combinator {
Some(c) => c.to_css(dest)?,
None => combinators_exhausted = true,
};
// to s.
if let Some(combinator) = next_combinator {
dest.write_char(' ')?;
combinator.to_css(dest)?;
}

// 4. If this is the last part of the chain of the selector and
// there is a pseudo-element, append "::" followed by the name of
// the pseudo-element, to s.
//
// (we handle this above)
}

Ok(())
}

fn serialize_component<'a, 'i, W>(
fn serialize_selector<'a, 'i, W>(
selector: &Selector<'i>,
dest: &mut Printer<W>,
context: Option<&StyleContext>,
mut is_relative: bool,
) -> Result<(), PrinterError>
where
W: fmt::Write,
{
serialize_selector_with_css_modules(selector, dest, context, is_relative, true)
}


fn serialize_component_with_css_modules<'a, 'i, W>(
component: &Component,
dest: &mut Printer<W>,
context: Option<&StyleContext>,
handle_css_modules: &mut bool,
) -> Result<(), PrinterError>
where
W: fmt::Write,
Expand Down Expand Up @@ -1598,7 +1607,7 @@ where
Component::Is(ref selectors) => {
// If there's only one simple selector, serialize it directly.
if should_unwrap_is(selectors) {
serialize_selector(selectors.first().unwrap(), dest, context, false)?;
serialize_selector_with_css_modules(selectors.first().unwrap(), dest, context, false, *handle_css_modules)?;
return Ok(());
}

Expand Down Expand Up @@ -1626,45 +1635,72 @@ where
}
_ => unreachable!(),
}
serialize_selector_list(list.iter(), dest, context, false)?;
serialize_selector_list_with_css_modules(list.iter(), dest, context, false, *handle_css_modules)?;
dest.write_str(")")
}
Component::Has(ref list) => {
dest.write_str(":has(")?;
serialize_selector_list(list.iter(), dest, context, true)?;
serialize_selector_list_with_css_modules(list.iter(), dest, context, true, *handle_css_modules)?;
dest.write_str(")")
}
Component::NonTSPseudoClass(pseudo) => serialize_pseudo_class(pseudo, dest, context),
Component::NonTSPseudoClass(pseudo) => {
// Intercept CSS Modules Local/Global to mutate hashing behavior for the remaining chain
match pseudo {
PseudoClass::Local { selector } => {
dest.write_str(":local(")?;
serialize_selector_with_css_modules(selector, dest, context, false, true)?;
dest.write_char(')')
}
PseudoClass::Global { selector } => {
// If it's the implicit non-functional :global alias (i.e. :global(&)),
// don't print the wrapper, just disable hashing for the rest of the chain.
let parts = selector.iter_raw_match_order().as_slice();
if parts.len() == 1 {
if let parcel_selectors::parser::Component::Nesting = parts[0] {
*handle_css_modules = false;
return Ok(());
}
}
// Otherwise, preserve explicit :global(<selector>) in the output
dest.write_str(":global(")?;
serialize_selector_with_css_modules(selector, dest, context, false, false)?;
dest.write_char(')')?;
*handle_css_modules = false;
Ok(())
}
_ => serialize_pseudo_class(pseudo, dest, context),
}
}
Component::PseudoElement(pseudo) => serialize_pseudo_element(pseudo, dest, context),
Component::Nesting => serialize_nesting(dest, context, false),
Component::Class(ref class) => {
dest.write_char('.')?;
dest.write_ident(&class.0, true)
dest.write_ident(&class.0, *handle_css_modules)
}
Component::ID(ref id) => {
dest.write_char('#')?;
dest.write_ident(&id.0, true)
dest.write_ident(&id.0, *handle_css_modules)
}
Component::Host(selector) => {
dest.write_str(":host")?;
if let Some(ref selector) = *selector {
dest.write_char('(')?;
selector.to_css(dest)?;
serialize_selector_with_css_modules(selector, dest, context, false, *handle_css_modules)?;
dest.write_char(')')?;
}
Ok(())
}
Component::Slotted(ref selector) => {
dest.write_str("::slotted(")?;
selector.to_css(dest)?;
serialize_selector_with_css_modules(selector, dest, context, false, *handle_css_modules)?;
dest.write_char(')')
}
Component::NthOf(ref nth_of_data) => {
let nth_data = nth_of_data.nth_data();
nth_data.write_start(dest, true)?;
nth_data.write_affine(dest)?;
dest.write_str(" of ")?;
serialize_selector_list(nth_of_data.selectors().iter(), dest, context, true)?;
serialize_selector_list_with_css_modules(nth_of_data.selectors().iter(), dest, context, true, *handle_css_modules)?;
dest.write_char(')')
}
_ => {
Expand All @@ -1674,6 +1710,41 @@ where
}
}

fn serialize_selector_list_with_css_modules<'a, 'i: 'a, I, W>(
iter: I,
dest: &mut Printer<W>,
context: Option<&StyleContext>,
is_relative: bool,
handle_css_modules: bool,
) -> Result<(), PrinterError>
where
I: Iterator<Item = &'a Selector<'i>>,
W: fmt::Write,
{
let mut first = true;
for selector in iter {
if !first {
dest.delim(',', false)?;
}
first = false;
serialize_selector_with_css_modules(selector, dest, context, is_relative, handle_css_modules)?;
}
Ok(())
}

fn serialize_component<'a, 'i, W>(
component: &Component,
dest: &mut Printer<W>,
context: Option<&StyleContext>,
) -> Result<(), PrinterError>
where
W: fmt::Write,
{
let mut handle = true;
serialize_component_with_css_modules(component, dest, context, &mut handle)
}


fn should_unwrap_is<'i>(selectors: &Box<[Selector<'i>]>) -> bool {
if selectors.len() == 1 {
let first = selectors.first().unwrap();
Expand Down