| //! Style rules. |
| |
| use std::hash::{Hash, Hasher}; |
| use std::ops::Range; |
| |
| use super::Location; |
| use super::MinifyContext; |
| use crate::context::DeclarationContext; |
| use crate::declaration::DeclarationBlock; |
| use crate::error::ParserError; |
| use crate::error::{MinifyError, PrinterError}; |
| use crate::parser::DefaultAtRule; |
| use crate::printer::Printer; |
| use crate::rules::CssRuleList; |
| use crate::selector::{ |
| downlevel_selectors, get_prefix, is_compatible, is_pure_css_modules_selector, is_unused, SelectorList, |
| }; |
| use crate::targets::{should_compile, Targets}; |
| use crate::traits::ToCss; |
| use crate::vendor_prefix::VendorPrefix; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use cssparser::*; |
| |
| /// A CSS [style rule](https://drafts.csswg.org/css-syntax/#style-rules). |
| #[derive(Debug, PartialEq, Clone)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct StyleRule<'i, R = DefaultAtRule> { |
| /// The selectors for the style rule. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| pub selectors: SelectorList<'i>, |
| /// A vendor prefix override, used during selector printing. |
| #[cfg_attr(feature = "serde", serde(skip, default = "VendorPrefix::empty"))] |
| #[cfg_attr(feature = "visitor", skip_visit)] |
| pub vendor_prefix: VendorPrefix, |
| /// The declarations within the style rule. |
| #[cfg_attr(feature = "serde", serde(default))] |
| pub declarations: DeclarationBlock<'i>, |
| /// Nested rules within the style rule. |
| #[cfg_attr(feature = "serde", serde(default = "default_rule_list::<R>"))] |
| pub rules: CssRuleList<'i, R>, |
| /// The location of the rule in the source file. |
| #[cfg_attr(feature = "visitor", skip_visit)] |
| pub loc: Location, |
| } |
| |
| #[cfg(feature = "serde")] |
| fn default_rule_list<'i, R>() -> CssRuleList<'i, R> { |
| CssRuleList(Vec::new()) |
| } |
| |
| impl<'i, T: Clone> StyleRule<'i, T> { |
| pub(crate) fn minify( |
| &mut self, |
| context: &mut MinifyContext<'_, 'i>, |
| parent_is_unused: bool, |
| ) -> Result<bool, MinifyError> { |
| let mut unused = false; |
| if !context.unused_symbols.is_empty() { |
| if is_unused(&mut self.selectors.0.iter(), &context.unused_symbols, parent_is_unused) { |
| if self.rules.0.is_empty() { |
| return Ok(true); |
| } |
| |
| self.declarations.declarations.clear(); |
| self.declarations.important_declarations.clear(); |
| unused = true; |
| } |
| } |
| |
| let pure_css_modules = context.pure_css_modules; |
| if context.pure_css_modules { |
| if !self.selectors.0.iter().all(is_pure_css_modules_selector) { |
| return Err(MinifyError { |
| kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector, |
| loc: self.loc, |
| }); |
| } |
| |
| // Parent rule contained id or class, so child rules don't need to. |
| context.pure_css_modules = false; |
| } |
| |
| context.handler_context.context = DeclarationContext::StyleRule; |
| self |
| .declarations |
| .minify(context.handler, context.important_handler, &mut context.handler_context); |
| context.handler_context.context = DeclarationContext::None; |
| |
| if !self.rules.0.is_empty() { |
| let mut handler_context = context.handler_context.child(DeclarationContext::StyleRule); |
| std::mem::swap(&mut context.handler_context, &mut handler_context); |
| self.rules.minify(context, unused)?; |
| context.handler_context = handler_context; |
| if unused && self.rules.0.is_empty() { |
| return Ok(true); |
| } |
| } |
| |
| context.pure_css_modules = pure_css_modules; |
| Ok(false) |
| } |
| } |
| |
| impl<'i, T> StyleRule<'i, T> { |
| /// Returns whether the rule is empty. |
| pub fn is_empty(&self) -> bool { |
| self.selectors.0.is_empty() || (self.declarations.is_empty() && self.rules.0.is_empty()) |
| } |
| |
| /// Returns whether the selectors in the rule are compatible |
| /// with all of the given browser targets. |
| pub fn is_compatible(&self, targets: Targets) -> bool { |
| is_compatible(&self.selectors.0, targets) |
| } |
| |
| /// Returns the line and column range of the property key and value at the given index in this style rule. |
| /// |
| /// For performance and memory efficiency in non-error cases, source locations are not stored during parsing. |
| /// Instead, they are computed lazily using the original source string that was used to parse the stylesheet/rule. |
| pub fn property_location<'t>( |
| &self, |
| code: &'i str, |
| index: usize, |
| ) -> Result<(Range<SourceLocation>, Range<SourceLocation>), ParseError<'i, ParserError<'i>>> { |
| let mut input = ParserInput::new(code); |
| let mut parser = Parser::new(&mut input); |
| |
| // advance until start location of this rule. |
| parse_at(&mut parser, self.loc, |parser| { |
| // skip selector |
| parser.parse_until_before(Delimiter::CurlyBracketBlock, |parser| { |
| while parser.next().is_ok() {} |
| Ok(()) |
| })?; |
| |
| parser.expect_curly_bracket_block()?; |
| parser.parse_nested_block(|parser| { |
| let loc = self.declarations.property_location(parser, index); |
| while parser.next().is_ok() {} |
| loc |
| }) |
| }) |
| } |
| |
| /// Returns a hash of this rule for use when deduplicating. |
| /// Includes the selectors and properties. |
| #[inline] |
| pub(crate) fn hash_key(&self) -> u64 { |
| let mut hasher = ahash::AHasher::default(); |
| self.selectors.hash(&mut hasher); |
| for (property, _) in self.declarations.iter() { |
| property.property_id().hash(&mut hasher); |
| } |
| hasher.finish() |
| } |
| |
| /// Returns whether this rule is a duplicate of another rule. |
| /// This means it has the same selectors and properties. |
| #[inline] |
| pub(crate) fn is_duplicate(&self, other_rule: &StyleRule<'i, T>) -> bool { |
| self.declarations.len() == other_rule.declarations.len() |
| && self.selectors == other_rule.selectors |
| && self |
| .declarations |
| .iter() |
| .zip(other_rule.declarations.iter()) |
| .all(|((a, _), (b, _))| a.property_id() == b.property_id()) |
| } |
| |
| pub(crate) fn update_prefix(&mut self, context: &mut MinifyContext<'_, 'i>) { |
| self.vendor_prefix = get_prefix(&self.selectors); |
| if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() { |
| self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), context.targets.current); |
| } |
| } |
| } |
| |
| fn parse_at<'i, 't, T, F>( |
| parser: &mut Parser<'i, 't>, |
| dest: Location, |
| parse: F, |
| ) -> Result<T, ParseError<'i, ParserError<'i>>> |
| where |
| F: Copy + for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, ParserError<'i>>>, |
| { |
| loop { |
| let loc = parser.current_source_location(); |
| if loc.line >= dest.line || (loc.line == dest.line && loc.column >= dest.column) { |
| return parse(parser); |
| } |
| |
| match parser.next()? { |
| Token::CurlyBracketBlock => { |
| // Recursively parse nested blocks. |
| let res = parser.parse_nested_block(|parser| { |
| let res = parse_at(parser, dest, parse); |
| while parser.next().is_ok() {} |
| res |
| }); |
| |
| if let Ok(v) = res { |
| return Ok(v); |
| } |
| } |
| _ => {} |
| } |
| } |
| } |
| |
| impl<'a, 'i, T: ToCss> ToCss for StyleRule<'i, T> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.vendor_prefix.is_empty() { |
| self.to_css_base(dest) |
| } else { |
| let mut first_rule = true; |
| for prefix in self.vendor_prefix { |
| if first_rule { |
| first_rule = false; |
| } else { |
| if !dest.minify { |
| dest.write_char('\n')?; // no indent |
| } |
| dest.newline()?; |
| } |
| dest.vendor_prefix = prefix; |
| self.to_css_base(dest)?; |
| } |
| |
| dest.vendor_prefix = VendorPrefix::empty(); |
| Ok(()) |
| } |
| } |
| } |
| |
| impl<'a, 'i, T: ToCss> StyleRule<'i, T> { |
| fn to_css_base<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. |
| let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets.current, Nesting); |
| let len = self.declarations.declarations.len() + self.declarations.important_declarations.len(); |
| let has_declarations = supports_nesting || len > 0 || self.rules.0.is_empty(); |
| |
| if has_declarations { |
| #[cfg(feature = "sourcemap")] |
| dest.add_mapping(self.loc); |
| self.selectors.to_css(dest)?; |
| dest.whitespace()?; |
| dest.write_char('{')?; |
| dest.indent(); |
| if len > 0 { |
| dest.newline()?; |
| } |
| |
| self.declarations.to_css_declarations( |
| dest, |
| supports_nesting && !self.rules.0.is_empty(), |
| &self.selectors, |
| self.loc.source_index, |
| )?; |
| } |
| |
| macro_rules! newline { |
| () => { |
| if !dest.minify && (supports_nesting || len > 0) && !self.rules.0.is_empty() { |
| if len > 0 { |
| dest.write_char('\n')?; |
| } |
| dest.newline()?; |
| } |
| }; |
| } |
| |
| macro_rules! end { |
| () => { |
| if has_declarations { |
| dest.dedent(); |
| dest.newline()?; |
| dest.write_char('}')?; |
| } |
| }; |
| } |
| |
| // Write nested rules after the parent. |
| if supports_nesting { |
| newline!(); |
| self.rules.to_css(dest)?; |
| end!(); |
| } else { |
| end!(); |
| newline!(); |
| dest.with_context(&self.selectors, |dest| self.rules.to_css(dest))?; |
| } |
| |
| Ok(()) |
| } |
| } |