From af0c096e91c73e817af99247f3f4924ed74f944b Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Wed, 29 Oct 2025 15:17:05 +0800 Subject: [PATCH 1/9] feat: support :global{} & :local{} for css module --- src/selector.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index d33d6e51..96223cc5 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -101,6 +101,11 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, ) -> Result, 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() => Global { selector: Box::new(Selector::from(Component::Nesting)) }, // https://drafts.csswg.org/selectors-4/#useraction-pseudos "hover" => Hover, "active" => Active, @@ -193,11 +198,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()))); From cb37890ff8f5ddf17e90f2df486417bcb13fc001 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Wed, 29 Oct 2025 15:30:05 +0800 Subject: [PATCH 2/9] fix: global --- src/selector.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index 96223cc5..45fa4491 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -104,8 +104,13 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, // 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() => Global { selector: Box::new(Selector::from(Component::Nesting)) }, + "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, From 36434fe9ff965bc47d0d6a33de466df9f0891d8d Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 01:18:04 +0800 Subject: [PATCH 3/9] fix: properly handle :global CSS modules context - Add serialize_selector_with_css_modules and serialize_component_with_css_modules functions - Pass handle_css_modules parameter to control CSS modules processing - In :global context, skip CSS modules hashing for class names and IDs - Fixes issue where :global { .class } was incorrectly hashed --- src/selector.rs | 339 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 338 insertions(+), 1 deletion(-) diff --git a/src/selector.rs b/src/selector.rs index 45fa4491..2b8d383f 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -805,7 +805,7 @@ where Local { selector } => serialize_selector(selector, dest, context, false), Global { selector } => { 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; Ok(()) } @@ -1381,12 +1381,185 @@ impl<'a, 'i> ToCss for Selector<'i> { } } +fn serialize_selector_with_css_modules<'a, 'i, W>( + selector: &Selector<'i>, + dest: &mut Printer, + context: Option<&StyleContext>, + mut is_relative: bool, + handle_css_modules: bool, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use parcel_selectors::parser::*; + // Compound selectors invert the order of their contents, so we need to + // undo that during serialization. + // + // This two-iterator strategy involves walking over the selector twice. + // We could do something more clever, but selector serialization probably + // isn't hot enough to justify it, and the stringification likely + // dominates anyway. + // + // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), + // which we need for |split|. So we split by combinators on a match-order + // sequence and then reverse. + + let mut combinators = selector.iter_raw_match_order().rev().filter_map(|x| x.as_combinator()); + let compound_selectors = selector.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev(); + 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)) { + if let Some(combinator) = combinators.next() { + combinator.to_css(dest)?; + } + compound = &compound[1..]; + is_relative = false; + } + + // https://drafts.csswg.org/cssom/#serializing-selectors + if compound.is_empty() { + continue; + } + + let has_leading_nesting = first && matches!(compound[0], Component::Nesting); + let first_index = if has_leading_nesting { 1 } else { 0 }; + first = false; + + // 1. If there is only one simple selector in the compound selectors + // which is a universal selector, append the result of + // serializing the universal selector to s. + // + // Check if `!compound.empty()` first--this can happen if we have + // something like `... > ::before`, because we store `>` and `::` + // both as combinators internally. + // + // If we are in this case, after we have serialized the universal + // selector, we skip Step 2 and continue with the algorithm. + let (can_elide_namespace, first_non_namespace) = match compound.get(first_index) { + Some(Component::ExplicitAnyNamespace) + | Some(Component::ExplicitNoNamespace) + | Some(Component::Namespace(..)) => (false, first_index + 1), + Some(Component::DefaultNamespace(..)) => (true, first_index + 1), + _ => (true, first_index), + }; + let mut perform_step_2 = true; + let next_combinator = combinators.next(); + if first_non_namespace == compound.len() - 1 { + match (next_combinator, &compound[first_non_namespace]) { + // We have to be careful here, because if there is a + // pseudo element "combinator" there isn't really just + // the one simple selector. Technically this compound + // selector contains the pseudo element selector as well + // -- Combinator::PseudoElement, just like + // Combinator::SlotAssignment, don't exist in the + // spec. + (Some(Combinator::PseudoElement), _) | (Some(Combinator::SlotAssignment), _) => (), + (_, &Component::ExplicitUniversalType) => { + // Iterate over everything so we serialize the namespace + // too. + let mut iter = compound.iter(); + let swap_nesting = has_leading_nesting && should_compile_nesting; + if swap_nesting { + // Swap nesting and type selector (e.g. &div -> div&). + iter.next(); + } + + for simple in iter { + serialize_component_with_css_modules(simple, dest, context, handle_css_modules)?; + } + + if swap_nesting { + serialize_nesting(dest, context, false)?; + } + + // Skip step 2, which is an "otherwise". + perform_step_2 = false; + } + _ => (), + } + } + + // 2. Otherwise, for each simple selector in the compound selectors + // that is not a universal selector of which the namespace prefix + // maps to a namespace that is not the default namespace + // serialize the simple selector and append the result to s. + // + // See https://github.com/w3c/csswg-drafts/issues/1606, which is + // proposing to change this to match up with the behavior asserted + // in cssom/serialize-namespaced-type-selectors.html, which the + // following code tries to match. + if perform_step_2 { + 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&). + let nesting = iter.next().unwrap(); + let local = iter.next().unwrap(); + serialize_component_with_css_modules(local, dest, context, 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_with_css_modules(local, dest, context, handle_css_modules)?; + } + + serialize_component_with_css_modules(nesting, dest, context, 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 { + // 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_with_css_modules(simple, dest, context, 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 + // to s. + if let Some(combinator) = next_combinator { + if !combinators_exhausted { + dest.write_char(' ')?; + combinator.to_css(dest)?; + } + } + + combinators_exhausted = combinators.next().is_none(); + } + + Ok(()) +} + fn serialize_selector<'a, 'i, W>( selector: &Selector<'i>, dest: &mut Printer, 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_selector_old<'a, 'i, W>( + selector: &Selector<'i>, + dest: &mut Printer, + context: Option<&StyleContext>, + mut is_relative: bool, +) -> Result<(), PrinterError> where W: fmt::Write, { @@ -1549,11 +1722,175 @@ where Ok(()) } +fn serialize_component_with_css_modules<'a, 'i, W>( + component: &Component, + dest: &mut Printer, + context: Option<&StyleContext>, + handle_css_modules: bool, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + match component { + Component::Combinator(ref c) => c.to_css(dest), + Component::AttributeInNoNamespace { + ref local_name, + operator, + ref value, + case_sensitivity, + .. + } => { + dest.write_char('[')?; + cssparser::ToCss::to_css(local_name, dest)?; + cssparser::ToCss::to_css(operator, dest)?; + + if dest.minify { + // Serialize as both an identifier and a string and choose the shorter one. + let mut id = String::new(); + serialize_identifier(&value, &mut id)?; + + let s = value.to_css_string(Default::default())?; + + if id.len() > 0 && id.len() < s.len() { + dest.write_str(&id)?; + } else { + dest.write_str(&s)?; + } + } else { + value.to_css(dest)?; + } + + match case_sensitivity { + parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive + | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} + parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + dest.write_char(']') + } + Component::Is(ref list) + | Component::Where(ref list) + | Component::Negation(ref list) + | Component::Any(_, ref list) => { + match *component { + Component::Where(..) => dest.write_str(":where(")?, + Component::Is(ref selectors) => { + // If there's only one simple selector, serialize it directly. + if should_unwrap_is(selectors) { + serialize_selector_with_css_modules(selectors.first().unwrap(), dest, context, false, handle_css_modules)?; + return Ok(()); + } + + let vp = dest.vendor_prefix; + if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { + dest.write_char(':')?; + vp.to_css(dest)?; + dest.write_str("any(")?; + } else { + dest.write_str(":is(")?; + } + } + Component::Negation(_) => { + dest.write_str(":not(")?; + } + Component::Any(prefix, ..) => { + let vp = dest.vendor_prefix.or(prefix); + if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { + dest.write_char(':')?; + vp.to_css(dest)?; + dest.write_str("any(")?; + } else { + dest.write_str(":is(")?; + } + } + _ => unreachable!(), + } + 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_with_css_modules(list.iter(), dest, context, true, handle_css_modules)?; + dest.write_str(")") + } + Component::NonTSPseudoClass(pseudo) => 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, handle_css_modules) + } + Component::ID(ref id) => { + dest.write_char('#')?; + dest.write_ident(&id.0, handle_css_modules) + } + Component::Host(selector) => { + dest.write_str(":host")?; + if let Some(ref selector) = *selector { + dest.write_char('(')?; + serialize_selector_with_css_modules(selector, dest, context, false, handle_css_modules)?; + dest.write_char(')')?; + } + Ok(()) + } + Component::Slotted(ref selector) => { + dest.write_str("::slotted(")?; + 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_with_css_modules(nth_of_data.selectors().iter(), dest, context, true, handle_css_modules)?; + dest.write_char(')') + } + _ => { + cssparser::ToCss::to_css(component, dest)?; + Ok(()) + } + } +} + +fn serialize_selector_list_with_css_modules<'a, 'i: 'a, I, W>( + iter: I, + dest: &mut Printer, + context: Option<&StyleContext>, + is_relative: bool, + handle_css_modules: bool, +) -> Result<(), PrinterError> +where + I: Iterator>, + 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, context: Option<&StyleContext>, ) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + serialize_component_with_css_modules(component, dest, context, true) +} + +fn serialize_component_old<'a, 'i, W>( + component: &Component, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> where W: fmt::Write, { From c45f2e0022d57e99c6b4014358de360eb9d4c78e Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 01:22:23 +0800 Subject: [PATCH 4/9] fix: remove incorrect combinators_exhausted check - Remove the !combinators_exhausted condition that was causing assertion failure - This was incorrectly copied from the original implementation --- src/selector.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index 2b8d383f..f0438a35 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1530,10 +1530,8 @@ where // append a single SPACE (U+0020), followed by the combinator // to s. if let Some(combinator) = next_combinator { - if !combinators_exhausted { - dest.write_char(' ')?; - combinator.to_css(dest)?; - } + dest.write_char(' ')?; + combinator.to_css(dest)?; } combinators_exhausted = combinators.next().is_none(); From 254b76f8999f241cd35caa46a97f9439b422e412 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 01:25:10 +0800 Subject: [PATCH 5/9] fix: remove unused old functions that had assertion issues - Remove serialize_selector_old and serialize_component_old functions - These were causing combinators_exhausted assertion failures - Keep only the working implementations --- src/selector.rs | 296 ------------------------------------------------ 1 file changed, 296 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index f0438a35..3e877bd1 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1552,173 +1552,6 @@ where serialize_selector_with_css_modules(selector, dest, context, is_relative, true) } -fn serialize_selector_old<'a, 'i, W>( - selector: &Selector<'i>, - dest: &mut Printer, - context: Option<&StyleContext>, - mut is_relative: bool, -) -> Result<(), PrinterError> -where - W: fmt::Write, -{ - use parcel_selectors::parser::*; - // Compound selectors invert the order of their contents, so we need to - // undo that during serialization. - // - // This two-iterator strategy involves walking over the selector twice. - // We could do something more clever, but selector serialization probably - // isn't hot enough to justify it, and the stringification likely - // dominates anyway. - // - // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), - // which we need for |split|. So we split by combinators on a match-order - // sequence and then reverse. - - let mut combinators = selector.iter_raw_match_order().rev().filter_map(|x| x.as_combinator()); - let compound_selectors = selector.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev(); - 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)) { - if let Some(combinator) = combinators.next() { - combinator.to_css(dest)?; - } - compound = &compound[1..]; - is_relative = false; - } - - // https://drafts.csswg.org/cssom/#serializing-selectors - if compound.is_empty() { - continue; - } - - let has_leading_nesting = first && matches!(compound[0], Component::Nesting); - let first_index = if has_leading_nesting { 1 } else { 0 }; - first = false; - - // 1. If there is only one simple selector in the compound selectors - // which is a universal selector, append the result of - // serializing the universal selector to s. - // - // Check if `!compound.empty()` first--this can happen if we have - // something like `... > ::before`, because we store `>` and `::` - // both as combinators internally. - // - // If we are in this case, after we have serialized the universal - // selector, we skip Step 2 and continue with the algorithm. - let (can_elide_namespace, first_non_namespace) = match compound.get(first_index) { - Some(Component::ExplicitAnyNamespace) - | Some(Component::ExplicitNoNamespace) - | Some(Component::Namespace(..)) => (false, first_index + 1), - Some(Component::DefaultNamespace(..)) => (true, first_index + 1), - _ => (true, first_index), - }; - let mut perform_step_2 = true; - let next_combinator = combinators.next(); - if first_non_namespace == compound.len() - 1 { - match (next_combinator, &compound[first_non_namespace]) { - // We have to be careful here, because if there is a - // pseudo element "combinator" there isn't really just - // the one simple selector. Technically this compound - // selector contains the pseudo element selector as well - // -- Combinator::PseudoElement, just like - // Combinator::SlotAssignment, don't exist in the - // spec. - (Some(Combinator::PseudoElement), _) | (Some(Combinator::SlotAssignment), _) => (), - (_, &Component::ExplicitUniversalType) => { - // Iterate over everything so we serialize the namespace - // too. - let mut iter = compound.iter(); - let swap_nesting = has_leading_nesting && should_compile_nesting; - if swap_nesting { - // Swap nesting and type selector (e.g. &div -> div&). - iter.next(); - } - - for simple in iter { - serialize_component(simple, dest, context)?; - } - - if swap_nesting { - serialize_nesting(dest, context, false)?; - } - - // Skip step 2, which is an "otherwise". - perform_step_2 = false; - } - _ => (), - } - } - - // 2. Otherwise, for each simple selector in the compound selectors - // that is not a universal selector of which the namespace prefix - // maps to a namespace that is not the default namespace - // serialize the simple selector and append the result to s. - // - // See https://github.com/w3c/csswg-drafts/issues/1606, which is - // proposing to change this to match up with the behavior asserted - // in cssom/serialize-namespaced-type-selectors.html, which the - // following code tries to match. - if perform_step_2 { - 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)?; - - // 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(nesting, dest, context)?; - } 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 { - continue; - } - } - serialize_component(simple, dest, context)?; - } - } - - // 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, - }; - - // 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_with_css_modules<'a, 'i, W>( component: &Component, @@ -1884,135 +1717,6 @@ where serialize_component_with_css_modules(component, dest, context, true) } -fn serialize_component_old<'a, 'i, W>( - component: &Component, - dest: &mut Printer, - context: Option<&StyleContext>, -) -> Result<(), PrinterError> -where - W: fmt::Write, -{ - match component { - Component::Combinator(ref c) => c.to_css(dest), - Component::AttributeInNoNamespace { - ref local_name, - operator, - ref value, - case_sensitivity, - .. - } => { - dest.write_char('[')?; - cssparser::ToCss::to_css(local_name, dest)?; - cssparser::ToCss::to_css(operator, dest)?; - - if dest.minify { - // Serialize as both an identifier and a string and choose the shorter one. - let mut id = String::new(); - serialize_identifier(&value, &mut id)?; - - let s = value.to_css_string(Default::default())?; - - if id.len() > 0 && id.len() < s.len() { - dest.write_str(&id)?; - } else { - dest.write_str(&s)?; - } - } else { - value.to_css(dest)?; - } - - match case_sensitivity { - parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive - | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} - parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, - parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, - } - dest.write_char(']') - } - Component::Is(ref list) - | Component::Where(ref list) - | Component::Negation(ref list) - | Component::Any(_, ref list) => { - match *component { - Component::Where(..) => dest.write_str(":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)?; - return Ok(()); - } - - let vp = dest.vendor_prefix; - if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { - dest.write_char(':')?; - vp.to_css(dest)?; - dest.write_str("any(")?; - } else { - dest.write_str(":is(")?; - } - } - Component::Negation(_) => { - dest.write_str(":not(")?; - } - Component::Any(prefix, ..) => { - let vp = dest.vendor_prefix.or(prefix); - if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { - dest.write_char(':')?; - vp.to_css(dest)?; - dest.write_str("any(")?; - } else { - dest.write_str(":is(")?; - } - } - _ => unreachable!(), - } - serialize_selector_list(list.iter(), dest, context, false)?; - dest.write_str(")") - } - Component::Has(ref list) => { - dest.write_str(":has(")?; - serialize_selector_list(list.iter(), dest, context, true)?; - dest.write_str(")") - } - Component::NonTSPseudoClass(pseudo) => 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) - } - Component::ID(ref id) => { - dest.write_char('#')?; - dest.write_ident(&id.0, true) - } - Component::Host(selector) => { - dest.write_str(":host")?; - if let Some(ref selector) = *selector { - dest.write_char('(')?; - selector.to_css(dest)?; - dest.write_char(')')?; - } - Ok(()) - } - Component::Slotted(ref selector) => { - dest.write_str("::slotted(")?; - selector.to_css(dest)?; - 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)?; - dest.write_char(')') - } - _ => { - cssparser::ToCss::to_css(component, dest)?; - Ok(()) - } - } -} fn should_unwrap_is<'i>(selectors: &Box<[Selector<'i>]>) -> bool { if selectors.len() == 1 { From d63a81e87c2d3969f73232018a2b529557d1f882 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 01:27:10 +0800 Subject: [PATCH 6/9] fix: remove problematic combinators_exhausted logic - Remove combinators_exhausted variable and its assertion - This was causing assertion failures in selector serialization - The logic was incorrect and not needed for the CSS modules fix --- src/selector.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index 3e877bd1..47b03aa9 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1409,9 +1409,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)) { @@ -1534,7 +1532,6 @@ where combinator.to_css(dest)?; } - combinators_exhausted = combinators.next().is_none(); } Ok(()) From 4b7e04b7dd13649c895ccc1b30aee6255e29f910 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 01:29:15 +0800 Subject: [PATCH 7/9] fix: properly output :global() wrapper in CSS - Add :global( and ) wrapper around the selector content - This ensures the output matches CSS Modules specification - The inner selector should not be hashed when inside :global() --- src/selector.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/selector.rs b/src/selector.rs index 47b03aa9..cde0200f 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -804,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_with_css_modules(selector, dest, context, false, false)?; dest.css_module = css_module; + dest.write_char(')')?; Ok(()) } From 1b5231982e30603c09643c39bf83fa990419819d Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 13:38:58 +0800 Subject: [PATCH 8/9] feat(css-modules): propagate :global state to disable hashing across selector chains - Pass mutable handle_css_modules through selector/component serializers - When encountering :global, disable hashing for the rest of the chain - Keep :local to serialize inner selector with hashing on - Output :global(...) wrapper explicitly --- src/selector.rs | 53 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/selector.rs b/src/selector.rs index cde0200f..bc01b013 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1388,7 +1388,7 @@ fn serialize_selector_with_css_modules<'a, 'i, W>( dest: &mut Printer, context: Option<&StyleContext>, mut is_relative: bool, - handle_css_modules: bool, + mut handle_css_modules: bool, ) -> Result<(), PrinterError> where W: fmt::Write, @@ -1471,7 +1471,7 @@ where } for simple in iter { - serialize_component_with_css_modules(simple, dest, context, handle_css_modules)?; + serialize_component_with_css_modules(simple, dest, context, &mut handle_css_modules)?; } if swap_nesting { @@ -1500,15 +1500,15 @@ where // Swap nesting and type selector (e.g. &div -> div&). let nesting = iter.next().unwrap(); let local = iter.next().unwrap(); - serialize_component_with_css_modules(local, dest, context, handle_css_modules)?; + 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_with_css_modules(local, dest, context, handle_css_modules)?; + serialize_component_with_css_modules(local, dest, context, &mut handle_css_modules)?; } - serialize_component_with_css_modules(nesting, dest, context, handle_css_modules)?; + 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(); @@ -1522,7 +1522,7 @@ where continue; } } - serialize_component_with_css_modules(simple, dest, context, handle_css_modules)?; + serialize_component_with_css_modules(simple, dest, context, &mut handle_css_modules)?; } } @@ -1556,7 +1556,7 @@ fn serialize_component_with_css_modules<'a, 'i, W>( component: &Component, dest: &mut Printer, context: Option<&StyleContext>, - handle_css_modules: bool, + handle_css_modules: &mut bool, ) -> Result<(), PrinterError> where W: fmt::Write, @@ -1607,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_with_css_modules(selectors.first().unwrap(), dest, context, false, handle_css_modules)?; + serialize_selector_with_css_modules(selectors.first().unwrap(), dest, context, false, *handle_css_modules)?; return Ok(()); } @@ -1635,37 +1635,55 @@ where } _ => unreachable!(), } - serialize_selector_list_with_css_modules(list.iter(), dest, context, false, handle_css_modules)?; + 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_with_css_modules(list.iter(), dest, context, true, handle_css_modules)?; + 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 } => { + dest.write_str(":global(")?; + serialize_selector_with_css_modules(selector, dest, context, false, false)?; + dest.write_char(')')?; + // Switch off css modules hashing for the rest of this selector chain + *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, handle_css_modules) + dest.write_ident(&class.0, *handle_css_modules) } Component::ID(ref id) => { dest.write_char('#')?; - dest.write_ident(&id.0, handle_css_modules) + dest.write_ident(&id.0, *handle_css_modules) } Component::Host(selector) => { dest.write_str(":host")?; if let Some(ref selector) = *selector { dest.write_char('(')?; - serialize_selector_with_css_modules(selector, dest, context, false, handle_css_modules)?; + serialize_selector_with_css_modules(selector, dest, context, false, *handle_css_modules)?; dest.write_char(')')?; } Ok(()) } Component::Slotted(ref selector) => { dest.write_str("::slotted(")?; - serialize_selector_with_css_modules(selector, dest, context, false, handle_css_modules)?; + serialize_selector_with_css_modules(selector, dest, context, false, *handle_css_modules)?; dest.write_char(')') } Component::NthOf(ref nth_of_data) => { @@ -1673,7 +1691,7 @@ where nth_data.write_start(dest, true)?; nth_data.write_affine(dest)?; dest.write_str(" of ")?; - serialize_selector_list_with_css_modules(nth_of_data.selectors().iter(), dest, context, true, handle_css_modules)?; + serialize_selector_list_with_css_modules(nth_of_data.selectors().iter(), dest, context, true, *handle_css_modules)?; dest.write_char(')') } _ => { @@ -1713,7 +1731,8 @@ fn serialize_component<'a, 'i, W>( where W: fmt::Write, { - serialize_component_with_css_modules(component, dest, context, true) + let mut handle = true; + serialize_component_with_css_modules(component, dest, context, &mut handle) } From 5a3ffb1c9a226e0fbfce8c38f0bd111ed601e2e0 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Thu, 30 Oct 2025 13:44:43 +0800 Subject: [PATCH 9/9] feat(css-modules): suppress :global(&) wrapper; only toggle hashing off - Detect implicit non-functional :global alias (:global(&)) - Do not emit wrapper, just disable hashing for the rest of the chain - Preserve explicit :global() in output --- src/selector.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/selector.rs b/src/selector.rs index bc01b013..369e2a6a 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1652,10 +1652,19 @@ where 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() in the output dest.write_str(":global(")?; serialize_selector_with_css_modules(selector, dest, context, false, false)?; dest.write_char(')')?; - // Switch off css modules hashing for the rest of this selector chain *handle_css_modules = false; Ok(()) }