//! CSS rules. //! //! The [CssRule](CssRule) enum includes all supported rules, and can be used to parse //! and serialize rules from CSS. Lists of rules (i.e. within a stylesheet, or inside //! another rule such as `@media`) are represented by [CssRuleList](CssRuleList). //! //! Each rule includes a source location, which indicates the line and column within //! the source file where it was parsed. This is used when generating source maps. //! //! # Example //! //! This example shows how you could parse a single CSS rule, and serialize it to a string. //! //! ``` //! use lightningcss::{ //! rules::CssRule, //! traits::ToCss, //! stylesheet::{ParserOptions, PrinterOptions} //! }; //! //! let rule = CssRule::parse_string( //! ".foo { color: red; }", //! ParserOptions::default() //! ).unwrap(); //! //! assert_eq!( //! rule.to_css_string(PrinterOptions::default()).unwrap(), //! ".foo {\n color: red;\n}" //! ); //! ``` //! //! If you have a [cssparser::Parser](cssparser::Parser) already, you can also use the `parse` and `to_css` //! methods instead, rather than parsing from a string. //! //! See [StyleSheet](super::stylesheet::StyleSheet) to parse an entire file of multiple rules. #![deny(missing_docs)] pub mod container; pub mod counter_style; pub mod custom_media; pub mod document; pub mod font_face; pub mod font_feature_values; pub mod font_palette_values; pub mod import; pub mod keyframes; pub mod layer; pub mod media; pub mod namespace; pub mod nesting; pub mod page; pub mod property; pub mod scope; pub mod starting_style; pub mod style; pub mod supports; pub mod unknown; pub mod view_transition; pub mod viewport; use self::font_feature_values::FontFeatureValuesRule; use self::font_palette_values::FontPaletteValuesRule; use self::layer::{LayerBlockRule, LayerStatementRule}; use self::property::PropertyRule; use crate::context::PropertyHandlerContext; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::dependencies::{Dependency, ImportDependency}; use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind}; use crate::parser::{parse_rule_list, parse_style_block, DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::prefixes::Feature; use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; use crate::selector::{is_compatible, is_equivalent, Component, Selector, SelectorList}; use crate::stylesheet::ParserOptions; use crate::targets::TargetsWithSupportsScope; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; use container::ContainerRule; use counter_style::CounterStyleRule; use cssparser::{parse_one_rule, ParseError, Parser, ParserInput}; use custom_media::CustomMediaRule; use document::MozDocumentRule; use font_face::FontFaceRule; use import::ImportRule; use itertools::Itertools; use keyframes::KeyframesRule; use media::MediaRule; use namespace::NamespaceRule; use nesting::{NestedDeclarationsRule, NestingRule}; use page::PageRule; use scope::ScopeRule; use smallvec::{smallvec, SmallVec}; use starting_style::StartingStyleRule; use std::collections::{HashMap, HashSet}; use std::hash::{BuildHasherDefault, Hasher}; use style::StyleRule; use supports::SupportsRule; use unknown::UnknownAtRule; use view_transition::ViewTransitionRule; use viewport::ViewportRule; #[derive(Clone)] pub(crate) struct StyleContext<'a, 'i> { pub selectors: &'a SelectorList<'i>, pub parent: Option<&'a StyleContext<'a, 'i>>, } /// A source location. #[derive(PartialEq, Eq, Debug, Clone, Copy)] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct Location { /// The index of the source file within the source map. pub source_index: u32, /// The line number, starting at 0. pub line: u32, /// The column number within a line, starting at 1 for first the character of the line. /// Column numbers are counted in UTF-16 code units. pub column: u32, } /// A CSS rule. #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "visitor", visit(visit_rule, RULES))] #[cfg_attr( feature = "serde", derive(serde::Serialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema), schemars(rename = "Rule"))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum CssRule<'i, R = DefaultAtRule> { /// A `@media` rule. #[cfg_attr(feature = "serde", serde(borrow))] Media(MediaRule<'i, R>), /// An `@import` rule. Import(ImportRule<'i>), /// A style rule. Style(StyleRule<'i, R>), /// A `@keyframes` rule. Keyframes(KeyframesRule<'i>), /// A `@font-face` rule. FontFace(FontFaceRule<'i>), /// A `@font-palette-values` rule. FontPaletteValues(FontPaletteValuesRule<'i>), /// A `@font-feature-values` rule. FontFeatureValues(FontFeatureValuesRule<'i>), /// A `@page` rule. Page(PageRule<'i>), /// A `@supports` rule. Supports(SupportsRule<'i, R>), /// A `@counter-style` rule. CounterStyle(CounterStyleRule<'i>), /// A `@namespace` rule. Namespace(NamespaceRule<'i>), /// A `@-moz-document` rule. MozDocument(MozDocumentRule<'i, R>), /// A `@nest` rule. Nesting(NestingRule<'i, R>), /// A nested declarations rule. NestedDeclarations(NestedDeclarationsRule<'i>), /// A `@viewport` rule. Viewport(ViewportRule<'i>), /// A `@custom-media` rule. CustomMedia(CustomMediaRule<'i>), /// A `@layer` statement rule. LayerStatement(LayerStatementRule<'i>), /// A `@layer` block rule. LayerBlock(LayerBlockRule<'i, R>), /// A `@property` rule. Property(PropertyRule<'i>), /// A `@container` rule. Container(ContainerRule<'i, R>), /// A `@scope` rule. Scope(ScopeRule<'i, R>), /// A `@starting-style` rule. StartingStyle(StartingStyleRule<'i, R>), /// A `@view-transition` rule. ViewTransition(ViewTransitionRule<'i>), /// A placeholder for a rule that was removed. Ignored, /// An unknown at-rule. Unknown(UnknownAtRule<'i>), /// A custom at-rule. Custom(R), } // Manually implemented deserialize to reduce binary size. #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRule<'i, R> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(serde::Deserialize)] #[serde(field_identifier, rename_all = "snake_case")] enum Field { Type, Value, } struct PartialRule<'de> { rule_type: CowArcStr<'de>, content: serde::__private::de::Content<'de>, } struct CssRuleVisitor; impl<'de> serde::de::Visitor<'de> for CssRuleVisitor { type Value = PartialRule<'de>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a CssRule") } fn visit_map(self, mut map: A) -> Result where A: serde::de::MapAccess<'de>, { let mut rule_type: Option> = None; let mut value: Option = None; while let Some(key) = map.next_key()? { match key { Field::Type => { rule_type = Some(map.next_value()?); } Field::Value => { value = Some(map.next_value()?); } } } let rule_type = rule_type.ok_or_else(|| serde::de::Error::missing_field("type"))?; let content = value.ok_or_else(|| serde::de::Error::missing_field("value"))?; Ok(PartialRule { rule_type, content }) } } let partial = deserializer.deserialize_map(CssRuleVisitor)?; let deserializer = serde::__private::de::ContentDeserializer::new(partial.content); match partial.rule_type.as_ref() { "media" => { let rule = MediaRule::deserialize(deserializer)?; Ok(CssRule::Media(rule)) } "import" => { let rule = ImportRule::deserialize(deserializer)?; Ok(CssRule::Import(rule)) } "style" => { let rule = StyleRule::deserialize(deserializer)?; Ok(CssRule::Style(rule)) } "keyframes" => { let rule = KeyframesRule::deserialize(deserializer)?; Ok(CssRule::Keyframes(rule)) } "font-face" => { let rule = FontFaceRule::deserialize(deserializer)?; Ok(CssRule::FontFace(rule)) } "font-palette-values" => { let rule = FontPaletteValuesRule::deserialize(deserializer)?; Ok(CssRule::FontPaletteValues(rule)) } "font-feature-values" => { let rule = FontFeatureValuesRule::deserialize(deserializer)?; Ok(CssRule::FontFeatureValues(rule)) } "page" => { let rule = PageRule::deserialize(deserializer)?; Ok(CssRule::Page(rule)) } "supports" => { let rule = SupportsRule::deserialize(deserializer)?; Ok(CssRule::Supports(rule)) } "counter-style" => { let rule = CounterStyleRule::deserialize(deserializer)?; Ok(CssRule::CounterStyle(rule)) } "namespace" => { let rule = NamespaceRule::deserialize(deserializer)?; Ok(CssRule::Namespace(rule)) } "moz-document" => { let rule = MozDocumentRule::deserialize(deserializer)?; Ok(CssRule::MozDocument(rule)) } "nesting" => { let rule = NestingRule::deserialize(deserializer)?; Ok(CssRule::Nesting(rule)) } "nested-declarations" => { let rule = NestedDeclarationsRule::deserialize(deserializer)?; Ok(CssRule::NestedDeclarations(rule)) } "viewport" => { let rule = ViewportRule::deserialize(deserializer)?; Ok(CssRule::Viewport(rule)) } "custom-media" => { let rule = CustomMediaRule::deserialize(deserializer)?; Ok(CssRule::CustomMedia(rule)) } "layer-statement" => { let rule = LayerStatementRule::deserialize(deserializer)?; Ok(CssRule::LayerStatement(rule)) } "layer-block" => { let rule = LayerBlockRule::deserialize(deserializer)?; Ok(CssRule::LayerBlock(rule)) } "property" => { let rule = PropertyRule::deserialize(deserializer)?; Ok(CssRule::Property(rule)) } "container" => { let rule = ContainerRule::deserialize(deserializer)?; Ok(CssRule::Container(rule)) } "scope" => { let rule = ScopeRule::deserialize(deserializer)?; Ok(CssRule::Scope(rule)) } "starting-style" => { let rule = StartingStyleRule::deserialize(deserializer)?; Ok(CssRule::StartingStyle(rule)) } "view-transition" => { let rule = ViewTransitionRule::deserialize(deserializer)?; Ok(CssRule::ViewTransition(rule)) } "ignored" => Ok(CssRule::Ignored), "unknown" => { let rule = UnknownAtRule::deserialize(deserializer)?; Ok(CssRule::Unknown(rule)) } "custom" => { let rule = R::deserialize(deserializer)?; Ok(CssRule::Custom(rule)) } t => Err(serde::de::Error::unknown_variant(t, &[])), } } } impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { CssRule::Media(media) => media.to_css(dest), CssRule::Import(import) => import.to_css(dest), CssRule::Style(style) => style.to_css(dest), CssRule::Keyframes(keyframes) => keyframes.to_css(dest), CssRule::FontFace(font_face) => font_face.to_css(dest), CssRule::FontPaletteValues(f) => f.to_css(dest), CssRule::FontFeatureValues(font_feature_values) => font_feature_values.to_css(dest), CssRule::Page(font_face) => font_face.to_css(dest), CssRule::Supports(supports) => supports.to_css(dest), CssRule::CounterStyle(counter_style) => counter_style.to_css(dest), CssRule::Namespace(namespace) => namespace.to_css(dest), CssRule::MozDocument(document) => document.to_css(dest), CssRule::Nesting(nesting) => nesting.to_css(dest), CssRule::NestedDeclarations(nested) => nested.to_css(dest), CssRule::Viewport(viewport) => viewport.to_css(dest), CssRule::CustomMedia(custom_media) => custom_media.to_css(dest), CssRule::LayerStatement(layer) => layer.to_css(dest), CssRule::LayerBlock(layer) => layer.to_css(dest), CssRule::Property(property) => property.to_css(dest), CssRule::StartingStyle(rule) => rule.to_css(dest), CssRule::Container(container) => container.to_css(dest), CssRule::Scope(scope) => scope.to_css(dest), CssRule::ViewTransition(rule) => rule.to_css(dest), CssRule::Unknown(unknown) => unknown.to_css(dest), CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError { kind: PrinterErrorKind::FmtError, loc: None, }), CssRule::Ignored => Ok(()), } } } impl<'i> CssRule<'i, DefaultAtRule> { /// Parse a single rule. pub fn parse<'t>( input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, ) -> Result>> { Self::parse_with(input, options, &mut DefaultAtRuleParser) } /// Parse a single rule from a string. pub fn parse_string( input: &'i str, options: ParserOptions<'_, 'i>, ) -> Result>> { Self::parse_string_with(input, options, &mut DefaultAtRuleParser) } } impl<'i, T> CssRule<'i, T> { /// Parse a single rule. pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, at_rule_parser: &mut P, ) -> Result>> { let mut rules = CssRuleList(Vec::new()); parse_one_rule(input, &mut TopLevelRuleParser::new(options, at_rule_parser, &mut rules))?; Ok(rules.0.pop().unwrap()) } /// Parse a single rule from a string. pub fn parse_string_with>( input: &'i str, options: ParserOptions<'_, 'i>, at_rule_parser: &mut P, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); Self::parse_with(&mut parser, &options, at_rule_parser) } } /// A list of CSS rules. #[derive(Debug, PartialEq, Clone, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct CssRuleList<'i, R = DefaultAtRule>( #[cfg_attr(feature = "serde", serde(borrow))] pub Vec>, ); impl<'i> CssRuleList<'i, DefaultAtRule> { /// Parse a rule list. pub fn parse<'t>( input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, ) -> Result>> { Self::parse_with(input, options, &mut DefaultAtRuleParser) } /// Parse a style block, with both declarations and rules. /// Resulting declarations are returned in a nested style rule. pub fn parse_style_block<'t>( input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, is_nested: bool, ) -> Result>> { Self::parse_style_block_with(input, options, &mut DefaultAtRuleParser, is_nested) } } impl<'i, T> CssRuleList<'i, T> { /// Parse a rule list with a custom at rule parser. pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, at_rule_parser: &mut P, ) -> Result>> { parse_rule_list(input, options, at_rule_parser) } /// Parse a style block, with both declarations and rules. /// Resulting declarations are returned in a nested style rule. pub fn parse_style_block_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, at_rule_parser: &mut P, is_nested: bool, ) -> Result>> { parse_style_block(input, options, at_rule_parser, is_nested) } } // Manually implemented to avoid circular child types. #[cfg(feature = "visitor")] #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for CssRuleList<'i, T> { const CHILD_TYPES: VisitTypes = VisitTypes::all(); fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> { if visitor.visit_types().contains(VisitTypes::RULES) { visitor.visit_rule_list(self) } else { self.0.visit(visitor) } } fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> { self.0.visit(visitor) } } pub(crate) struct MinifyContext<'a, 'i> { pub targets: TargetsWithSupportsScope, pub handler: &'a mut DeclarationHandler<'i>, pub important_handler: &'a mut DeclarationHandler<'i>, pub handler_context: PropertyHandlerContext<'i, 'a>, pub unused_symbols: &'a HashSet, pub custom_media: Option, CustomMediaRule<'i>>>, pub css_modules: bool, pub pure_css_modules: bool, } impl<'i, T: Clone> CssRuleList<'i, T> { pub(crate) fn minify( &mut self, context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool, ) -> Result<(), MinifyError> { let mut keyframe_rules = HashMap::new(); let mut layer_rules = HashMap::new(); let mut has_layers = false; let mut property_rules = HashMap::new(); let mut font_feature_values_rules = Vec::new(); let mut style_rules = HashMap::with_capacity_and_hasher(self.0.len(), BuildHasherDefault::::default()); let mut rules = Vec::new(); for mut rule in self.0.drain(..) { match &mut rule { CssRule::Keyframes(keyframes) => { if context.unused_symbols.contains(match &keyframes.name { KeyframesName::Ident(ident) => ident.0.as_ref(), KeyframesName::Custom(string) => string.as_ref(), }) { continue; } keyframes.minify(context); macro_rules! set_prefix { ($keyframes: ident) => { $keyframes.vendor_prefix = context.targets.current.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes); }; } // Merge @keyframes rules with the same name. if let Some(existing_idx) = keyframe_rules.get(&keyframes.name) { if let Some(CssRule::Keyframes(existing)) = &mut rules.get_mut(*existing_idx) { // If the existing rule has the same vendor prefixes, replace it with this rule. if existing.vendor_prefix == keyframes.vendor_prefix { *existing = keyframes.clone(); continue; } // Otherwise, if the keyframes are identical, merge the prefixes. if existing.keyframes == keyframes.keyframes { existing.vendor_prefix |= keyframes.vendor_prefix; set_prefix!(existing); continue; } } } set_prefix!(keyframes); keyframe_rules.insert(keyframes.name.clone(), rules.len()); let fallbacks = keyframes.get_fallbacks(&context.targets.current); rules.push(rule); rules.extend(fallbacks); continue; } CssRule::CustomMedia(_) => { if context.custom_media.is_some() { continue; } } CssRule::Media(media) => { if let Some(CssRule::Media(last_rule)) = rules.last_mut() { if last_rule.query == media.query { last_rule.rules.0.extend(media.rules.0.drain(..)); last_rule.minify(context, parent_is_unused)?; continue; } } if media.minify(context, parent_is_unused)? { continue; } } CssRule::Supports(supports) => { if let Some(CssRule::Supports(last_rule)) = rules.last_mut() { if last_rule.condition == supports.condition { last_rule.rules.0.extend(supports.rules.0.drain(..)); last_rule.minify(context, parent_is_unused)?; continue; } } supports.minify(context, parent_is_unused)?; if supports.rules.0.is_empty() { continue; } } CssRule::Container(container) => { if let Some(CssRule::Container(last_rule)) = rules.last_mut() { if last_rule.name == container.name && last_rule.condition == container.condition { last_rule.rules.0.extend(container.rules.0.drain(..)); last_rule.minify(context, parent_is_unused)?; continue; } } if container.minify(context, parent_is_unused)? { continue; } } CssRule::LayerBlock(layer) => { // Merging non-adjacent layer rules is safe because they are applied // in the order they are first defined. if let Some(name) = &layer.name { if let Some(idx) = layer_rules.get(name) { if let Some(CssRule::LayerBlock(last_rule)) = rules.get_mut(*idx) { last_rule.rules.0.extend(layer.rules.0.drain(..)); continue; } } layer_rules.insert(name.clone(), rules.len()); has_layers = true; } } CssRule::LayerStatement(layer) => { // Create @layer block rules for each declared layer name, // so we can merge other blocks into it later on. for name in &layer.names { if !layer_rules.contains_key(name) { layer_rules.insert(name.clone(), rules.len()); has_layers = true; rules.push(CssRule::LayerBlock(LayerBlockRule { name: Some(name.clone()), rules: CssRuleList(vec![]), loc: layer.loc.clone(), })); } } continue; } CssRule::MozDocument(document) => document.minify(context)?, CssRule::Style(style) => { if parent_is_unused || style.minify(context, parent_is_unused)? { continue; } // If some of the selectors in this rule are not compatible with the targets, // we need to either wrap in :is() or split them into multiple rules. let incompatible = if style.selectors.0.len() > 1 && context.targets.current.should_compile_selectors() && !style.is_compatible(context.targets.current) { // The :is() selector accepts a forgiving selector list, so use that if possible. // Note that :is() does not allow pseudo elements, so we need to check for that. // In addition, :is() takes the highest specificity of its arguments, so if the selectors // have different weights, we need to split them into separate rules as well. if context.targets.current.is_compatible(crate::compat::Feature::IsSelector) && !style.selectors.0.iter().any(|selector| selector.has_pseudo_element()) && style.selectors.0.iter().map(|selector| selector.specificity()).all_equal() { style.selectors = SelectorList::new(smallvec![ Component::Is(style.selectors.0.clone().into_boxed_slice()).into() ]); smallvec![] } else { // Otherwise, partition the selectors and keep the compatible ones in this rule. // We will generate additional rules for incompatible selectors later. let (compatible, incompatible) = style .selectors .0 .iter() .cloned() .partition::, _>(|selector| { let list = SelectorList::new(smallvec![selector.clone()]); is_compatible(&list.0, context.targets.current) }); style.selectors = SelectorList::new(compatible); incompatible } } else { smallvec![] }; style.update_prefix(context); // Attempt to merge the new rule with the last rule we added. let mut merged = false; if let Some(CssRule::Style(last_style_rule)) = rules.last_mut() { if merge_style_rules(style, last_style_rule, context) { // If that was successful, then the last rule has been updated to include the // selectors/declarations of the new rule. This might mean that we can merge it // with the previous rule, so continue trying while we have style rules available. while rules.len() >= 2 { let len = rules.len(); let (a, b) = rules.split_at_mut(len - 1); if let (CssRule::Style(last), CssRule::Style(prev)) = (&mut b[0], &mut a[len - 2]) { if merge_style_rules(last, prev, context) { // If we were able to merge the last rule into the previous one, remove the last. rules.pop(); continue; } } // If we didn't see a style rule, or were unable to merge, stop. break; } merged = true; } } // Create additional rules for logical properties, @supports overrides, and incompatible selectors. let supports = context.handler_context.get_supports_rules(&style); let logical = context.handler_context.get_additional_rules(&style); let incompatible_rules = incompatible .into_iter() .map(|selector| { // Create a clone of the rule with only the one incompatible selector. let list = SelectorList::new(smallvec![selector]); let mut clone = style.clone(); clone.selectors = list; clone.update_prefix(context); // Also add rules for logical properties and @supports overrides. let supports = context.handler_context.get_supports_rules(&clone); let logical = context.handler_context.get_additional_rules(&clone); (clone, logical, supports) }) .collect::>(); context.handler_context.reset(); // If the rule has nested rules, and we have extra rules to insert such as for logical properties, // we need to split the rule in two so we can insert the extra rules in between the declarations from // the main rule and the nested rules. let nested_rule = if !style.rules.0.is_empty() // can happen if there are no compatible rules, above. && !style.selectors.0.is_empty() && (!logical.is_empty() || !supports.is_empty() || !incompatible_rules.is_empty()) { let mut rules = CssRuleList(vec![]); std::mem::swap(&mut style.rules, &mut rules); Some(StyleRule { selectors: style.selectors.clone(), declarations: DeclarationBlock::default(), rules, vendor_prefix: style.vendor_prefix, loc: style.loc, }) } else { None }; if !merged && !style.is_empty() { let source_index = style.loc.source_index; let has_no_rules = style.rules.0.is_empty(); let idx = rules.len(); rules.push(rule); // Check if this rule is a duplicate of an earlier rule, meaning it has // the same selectors and defines the same properties. If so, remove the // earlier rule because this one completely overrides it. if has_no_rules { // SAFETY: StyleRuleKeys never live beyond this method. let key = StyleRuleKey::new(unsafe { &*(&rules as *const _) }, idx); if idx > 0 { if let Some(i) = style_rules.remove(&key) { if let CssRule::Style(other) = &rules[i] { // Don't remove the rule if this is a CSS module and the other rule came from a different file. if !context.css_modules || source_index == other.loc.source_index { // Only mark the rule as ignored so we don't need to change all of the indices. rules[i] = CssRule::Ignored; } } } } style_rules.insert(key, idx); } } if !logical.is_empty() { let mut logical = CssRuleList(logical); logical.minify(context, parent_is_unused)?; rules.extend(logical.0) } rules.extend(supports); for (rule, logical, supports) in incompatible_rules { if !rule.is_empty() { rules.push(CssRule::Style(rule)); } if !logical.is_empty() { let mut logical = CssRuleList(logical); logical.minify(context, parent_is_unused)?; rules.extend(logical.0) } rules.extend(supports); } if let Some(nested_rule) = nested_rule { rules.push(CssRule::Style(nested_rule)); } continue; } CssRule::CounterStyle(counter_style) => { if context.unused_symbols.contains(counter_style.name.0.as_ref()) { continue; } } CssRule::Scope(scope) => scope.minify(context)?, CssRule::Nesting(nesting) => { if nesting.minify(context, parent_is_unused)? { continue; } } CssRule::NestedDeclarations(nested) => { if nested.minify(context, parent_is_unused) { continue; } } CssRule::StartingStyle(rule) => { if rule.minify(context, parent_is_unused)? { continue; } } CssRule::FontPaletteValues(f) => { if context.unused_symbols.contains(f.name.0.as_ref()) { continue; } f.minify(context, parent_is_unused); let fallbacks = f.get_fallbacks(context.targets.current); rules.push(rule); rules.extend(fallbacks); continue; } CssRule::FontFeatureValues(rule) => { if let Some(index) = font_feature_values_rules .iter() .find(|index| matches!(&rules[**index], CssRule::FontFeatureValues(r) if r.name == rule.name)) { if let CssRule::FontFeatureValues(existing) = &mut rules[*index] { existing.merge(rule); } continue; } else { font_feature_values_rules.push(rules.len()); } } CssRule::Property(property) => { if context.unused_symbols.contains(property.name.0.as_ref()) { continue; } if let Some(index) = property_rules.get(&property.name) { rules[*index] = rule; continue; } else { property_rules.insert(property.name.clone(), rules.len()); } } CssRule::Import(_) => { // @layer blocks can't be inlined into layers declared before imports. layer_rules.clear(); } _ => {} } rules.push(rule) } // Optimize @layer rules. Combine subsequent empty layer blocks into a single @layer statement // so that layers are declared in the correct order. if has_layers { let mut declared_layers = HashSet::new(); let mut layer_statement = None; for index in 0..rules.len() { match &mut rules[index] { CssRule::LayerBlock(layer) => { if layer.minify(context, parent_is_unused)? { if let Some(name) = &layer.name { if declared_layers.contains(name) { // Remove empty layer that has already been declared. rules[index] = CssRule::Ignored; continue; } let name = name.clone(); declared_layers.insert(name.clone()); if let Some(layer_index) = layer_statement { if let CssRule::LayerStatement(layer) = &mut rules[layer_index] { // Add name to previous layer statement rule and remove this one. layer.names.push(name); rules[index] = CssRule::Ignored; } } else { // Create a new layer statement rule to declare the name. rules[index] = CssRule::LayerStatement(LayerStatementRule { names: vec![name], loc: layer.loc, }); layer_statement = Some(index); } } else { // Remove empty anonymous layer. rules[index] = CssRule::Ignored; } } else { // Non-empty @layer block. Start a new statement. layer_statement = None; } } CssRule::Import(import) => { if let Some(layer) = &import.layer { // Start a new @layer statement so the import layer is in the right order. layer_statement = None; if let Some(name) = layer { declared_layers.insert(name.clone()); } } } _ => {} } } } self.0 = rules; Ok(()) } } fn merge_style_rules<'i, T>( style: &mut StyleRule<'i, T>, last_style_rule: &mut StyleRule<'i, T>, context: &mut MinifyContext<'_, 'i>, ) -> bool { // Merge declarations if the selectors are equivalent, and both are compatible with all targets. if style.selectors == last_style_rule.selectors && style.is_compatible(context.targets.current) && last_style_rule.is_compatible(context.targets.current) && style.rules.0.is_empty() && last_style_rule.rules.0.is_empty() && (!context.css_modules || style.loc.source_index == last_style_rule.loc.source_index) { last_style_rule .declarations .declarations .extend(style.declarations.declarations.drain(..)); last_style_rule .declarations .important_declarations .extend(style.declarations.important_declarations.drain(..)); last_style_rule .declarations .minify(context.handler, context.important_handler, &mut context.handler_context); return true; } else if style.declarations == last_style_rule.declarations && style.rules.0.is_empty() && last_style_rule.rules.0.is_empty() { // If both selectors are potentially vendor prefixable, and they are // equivalent minus prefixes, add the prefix to the last rule. if !style.vendor_prefix.is_empty() && !last_style_rule.vendor_prefix.is_empty() && is_equivalent(&style.selectors.0, &last_style_rule.selectors.0) { // If the new rule is unprefixed, replace the prefixes of the last rule. // Otherwise, add the new prefix. if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { last_style_rule.vendor_prefix = style.vendor_prefix; } else { last_style_rule.vendor_prefix |= style.vendor_prefix; } return true; } // Append the selectors to the last rule if the declarations are the same, and all selectors are compatible. if style.is_compatible(context.targets.current) && last_style_rule.is_compatible(context.targets.current) { last_style_rule.selectors.0.extend(style.selectors.0.drain(..)); if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { last_style_rule.vendor_prefix = style.vendor_prefix; } else { last_style_rule.vendor_prefix |= style.vendor_prefix; } return true; } } false } impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { let mut first = true; let mut last_without_block = false; for rule in &self.0 { if let CssRule::Ignored = &rule { continue; } // Skip @import rules if collecting dependencies. if let CssRule::Import(rule) = &rule { if dest.remove_imports { let dep = if dest.dependencies.is_some() { Some(Dependency::Import(ImportDependency::new(&rule, dest.filename()))) } else { None }; if let Some(dependencies) = &mut dest.dependencies { dependencies.push(dep.unwrap()); continue; } } } if first { first = false; } else { if !dest.minify && !(last_without_block && matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) )) { dest.write_char('\n')?; } dest.newline()?; } rule.to_css(dest)?; last_without_block = matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) ); } Ok(()) } } impl<'i, T> std::ops::Index for CssRuleList<'i, T> { type Output = CssRule<'i, T>; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } impl<'i, T> std::ops::IndexMut for CssRuleList<'i, T> { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] } } /// A hasher that expects to be called with a single u64, which is already a hash. #[derive(Default)] struct PrecomputedHasher { hash: Option, } impl Hasher for PrecomputedHasher { #[inline] fn write(&mut self, _: &[u8]) { unreachable!() } #[inline] fn write_u64(&mut self, i: u64) { debug_assert!(self.hash.is_none()); self.hash = Some(i); } #[inline] fn finish(&self) -> u64 { self.hash.unwrap() } } /// A key to a StyleRule meant for use in a HashMap for quickly detecting duplicates. /// It stores a reference to a list and an index so it can access items without cloning /// even when the list is reallocated. A hash is also pre-computed for fast lookups. #[derive(Clone)] pub(crate) struct StyleRuleKey<'a, 'i, R> { list: &'a Vec>, index: usize, hash: u64, } impl<'a, 'i, R> StyleRuleKey<'a, 'i, R> { fn new(list: &'a Vec>, index: usize) -> Self { let rule = match &list[index] { CssRule::Style(style) => style, _ => unreachable!(), }; Self { list, index, hash: rule.hash_key(), } } } impl<'a, 'i, R> PartialEq for StyleRuleKey<'a, 'i, R> { fn eq(&self, other: &Self) -> bool { let rule = match self.list.get(self.index) { Some(CssRule::Style(style)) => style, _ => return false, }; let other_rule = match other.list.get(other.index) { Some(CssRule::Style(style)) => style, _ => return false, }; rule.is_duplicate(other_rule) } } impl<'a, 'i, R> Eq for StyleRuleKey<'a, 'i, R> {} impl<'a, 'i, R> std::hash::Hash for StyleRuleKey<'a, 'i, R> { fn hash(&self, state: &mut H) { state.write_u64(self.hash); } }