lightningcss/rules/
style.rs

1//! Style rules.
2
3use std::hash::{Hash, Hasher};
4use std::ops::Range;
5
6use super::Location;
7use super::MinifyContext;
8use crate::context::DeclarationContext;
9use crate::declaration::DeclarationBlock;
10use crate::error::ParserError;
11use crate::error::{MinifyError, PrinterError, PrinterErrorKind};
12use crate::parser::DefaultAtRule;
13use crate::printer::Printer;
14use crate::rules::CssRuleList;
15use crate::selector::{
16  downlevel_selectors, get_prefix, is_compatible, is_pure_css_modules_selector, is_unused, SelectorList,
17};
18use crate::targets::{should_compile, Targets};
19use crate::traits::ToCss;
20use crate::vendor_prefix::VendorPrefix;
21#[cfg(feature = "visitor")]
22use crate::visitor::Visit;
23use cssparser::*;
24
25/// A CSS [style rule](https://drafts.csswg.org/css-syntax/#style-rules).
26#[derive(Debug, PartialEq, Clone)]
27#[cfg_attr(feature = "visitor", derive(Visit))]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
30#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
31pub struct StyleRule<'i, R = DefaultAtRule> {
32  /// The selectors for the style rule.
33  #[cfg_attr(feature = "serde", serde(borrow))]
34  pub selectors: SelectorList<'i>,
35  /// A vendor prefix override, used during selector printing.
36  #[cfg_attr(feature = "serde", serde(skip, default = "VendorPrefix::empty"))]
37  #[cfg_attr(feature = "visitor", skip_visit)]
38  pub vendor_prefix: VendorPrefix,
39  /// The declarations within the style rule.
40  #[cfg_attr(feature = "serde", serde(default))]
41  pub declarations: DeclarationBlock<'i>,
42  /// Nested rules within the style rule.
43  #[cfg_attr(feature = "serde", serde(default = "default_rule_list::<R>"))]
44  pub rules: CssRuleList<'i, R>,
45  /// The location of the rule in the source file.
46  #[cfg_attr(feature = "visitor", skip_visit)]
47  pub loc: Location,
48}
49
50#[cfg(feature = "serde")]
51fn default_rule_list<'i, R>() -> CssRuleList<'i, R> {
52  CssRuleList(Vec::new())
53}
54
55impl<'i, T: Clone> StyleRule<'i, T> {
56  pub(crate) fn minify(
57    &mut self,
58    context: &mut MinifyContext<'_, 'i>,
59    parent_is_unused: bool,
60  ) -> Result<bool, MinifyError> {
61    let mut unused = false;
62    if !context.unused_symbols.is_empty() {
63      if is_unused(&mut self.selectors.0.iter(), &context.unused_symbols, parent_is_unused) {
64        if self.rules.0.is_empty() {
65          return Ok(true);
66        }
67
68        self.declarations.declarations.clear();
69        self.declarations.important_declarations.clear();
70        unused = true;
71      }
72    }
73
74    let pure_css_modules = context.pure_css_modules;
75    if context.pure_css_modules {
76      if !self.selectors.0.iter().all(is_pure_css_modules_selector) {
77        return Err(MinifyError {
78          kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector,
79          loc: self.loc,
80        });
81      }
82
83      // Parent rule contained id or class, so child rules don't need to.
84      context.pure_css_modules = false;
85    }
86
87    context.handler_context.context = DeclarationContext::StyleRule;
88    self
89      .declarations
90      .minify(context.handler, context.important_handler, &mut context.handler_context);
91    context.handler_context.context = DeclarationContext::None;
92
93    if !self.rules.0.is_empty() {
94      let mut handler_context = context.handler_context.child(DeclarationContext::StyleRule);
95      std::mem::swap(&mut context.handler_context, &mut handler_context);
96      self.rules.minify(context, unused)?;
97      context.handler_context = handler_context;
98      if unused && self.rules.0.is_empty() {
99        return Ok(true);
100      }
101    }
102
103    context.pure_css_modules = pure_css_modules;
104    Ok(false)
105  }
106}
107
108impl<'i, T> StyleRule<'i, T> {
109  /// Returns whether the rule is empty.
110  pub fn is_empty(&self) -> bool {
111    self.selectors.0.is_empty() || (self.declarations.is_empty() && self.rules.0.is_empty())
112  }
113
114  /// Returns whether the selectors in the rule are compatible
115  /// with all of the given browser targets.
116  pub fn is_compatible(&self, targets: Targets) -> bool {
117    is_compatible(&self.selectors.0, targets)
118  }
119
120  /// Returns the line and column range of the property key and value at the given index in this style rule.
121  ///
122  /// For performance and memory efficiency in non-error cases, source locations are not stored during parsing.
123  /// Instead, they are computed lazily using the original source string that was used to parse the stylesheet/rule.
124  pub fn property_location<'t>(
125    &self,
126    code: &'i str,
127    index: usize,
128  ) -> Result<(Range<SourceLocation>, Range<SourceLocation>), ParseError<'i, ParserError<'i>>> {
129    let mut input = ParserInput::new(code);
130    let mut parser = Parser::new(&mut input);
131
132    // advance until start location of this rule.
133    parse_at(&mut parser, self.loc, |parser| {
134      // skip selector
135      parser.parse_until_before(Delimiter::CurlyBracketBlock, |parser| {
136        while parser.next().is_ok() {}
137        Ok(())
138      })?;
139
140      parser.expect_curly_bracket_block()?;
141      parser.parse_nested_block(|parser| {
142        let loc = self.declarations.property_location(parser, index);
143        while parser.next().is_ok() {}
144        loc
145      })
146    })
147  }
148
149  /// Returns a hash of this rule for use when deduplicating.
150  /// Includes the selectors and properties.
151  #[inline]
152  pub(crate) fn hash_key(&self) -> u64 {
153    let mut hasher = ahash::AHasher::default();
154    self.selectors.hash(&mut hasher);
155    for (property, _) in self.declarations.iter() {
156      property.property_id().hash(&mut hasher);
157    }
158    hasher.finish()
159  }
160
161  /// Returns whether this rule is a duplicate of another rule.
162  /// This means it has the same selectors and properties.
163  #[inline]
164  pub(crate) fn is_duplicate(&self, other_rule: &StyleRule<'i, T>) -> bool {
165    self.declarations.len() == other_rule.declarations.len()
166      && self.selectors == other_rule.selectors
167      && self
168        .declarations
169        .iter()
170        .zip(other_rule.declarations.iter())
171        .all(|((a, _), (b, _))| a.property_id() == b.property_id())
172  }
173
174  pub(crate) fn update_prefix(&mut self, context: &mut MinifyContext<'_, 'i>) {
175    self.vendor_prefix = get_prefix(&self.selectors);
176    if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.should_compile_selectors() {
177      self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), *context.targets);
178    }
179  }
180}
181
182fn parse_at<'i, 't, T, F>(
183  parser: &mut Parser<'i, 't>,
184  dest: Location,
185  parse: F,
186) -> Result<T, ParseError<'i, ParserError<'i>>>
187where
188  F: Copy + for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, ParserError<'i>>>,
189{
190  loop {
191    let loc = parser.current_source_location();
192    if loc.line >= dest.line || (loc.line == dest.line && loc.column >= dest.column) {
193      return parse(parser);
194    }
195
196    match parser.next()? {
197      Token::CurlyBracketBlock => {
198        // Recursively parse nested blocks.
199        let res = parser.parse_nested_block(|parser| {
200          let res = parse_at(parser, dest, parse);
201          while parser.next().is_ok() {}
202          res
203        });
204
205        if let Ok(v) = res {
206          return Ok(v);
207        }
208      }
209      _ => {}
210    }
211  }
212}
213
214impl<'a, 'i, T: ToCss> ToCss for StyleRule<'i, T> {
215  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
216  where
217    W: std::fmt::Write,
218  {
219    if self.vendor_prefix.is_empty() {
220      self.to_css_base(dest)
221    } else {
222      let mut first_rule = true;
223      for prefix in self.vendor_prefix {
224        if first_rule {
225          first_rule = false;
226        } else {
227          if !dest.minify {
228            dest.write_char('\n')?; // no indent
229          }
230          dest.newline()?;
231        }
232        dest.vendor_prefix = prefix;
233        self.to_css_base(dest)?;
234      }
235
236      dest.vendor_prefix = VendorPrefix::empty();
237      Ok(())
238    }
239  }
240}
241
242impl<'a, 'i, T: ToCss> StyleRule<'i, T> {
243  fn to_css_base<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
244  where
245    W: std::fmt::Write,
246  {
247    // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent.
248    let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets, Nesting);
249    let len = self.declarations.declarations.len() + self.declarations.important_declarations.len();
250    let has_declarations = supports_nesting || len > 0 || self.rules.0.is_empty();
251
252    if has_declarations {
253      #[cfg(feature = "sourcemap")]
254      dest.add_mapping(self.loc);
255      self.selectors.to_css(dest)?;
256      dest.whitespace()?;
257      dest.write_char('{')?;
258      dest.indent();
259
260      let mut i = 0;
261      macro_rules! write {
262        ($decls: ident, $important: literal) => {
263          for decl in &self.declarations.$decls {
264            // The CSS modules `composes` property is handled specially, and omitted during printing.
265            // We need to add the classes it references to the list for the selectors in this rule.
266            if let crate::properties::Property::Composes(composes) = &decl {
267              if dest.is_nested() && dest.css_module.is_some() {
268                return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc));
269              }
270
271              if let Some(css_module) = &mut dest.css_module {
272                css_module
273                  .handle_composes(&self.selectors, &composes, self.loc.source_index)
274                  .map_err(|e| dest.error(e, composes.loc))?;
275                continue;
276              }
277            }
278
279            dest.newline()?;
280            decl.to_css(dest, $important)?;
281            if i != len - 1 || !dest.minify || (supports_nesting && !self.rules.0.is_empty()) {
282              dest.write_char(';')?;
283            }
284
285            i += 1;
286          }
287        };
288      }
289
290      write!(declarations, false);
291      write!(important_declarations, true);
292    }
293
294    macro_rules! newline {
295      () => {
296        if !dest.minify && (supports_nesting || len > 0) && !self.rules.0.is_empty() {
297          if len > 0 {
298            dest.write_char('\n')?;
299          }
300          dest.newline()?;
301        }
302      };
303    }
304
305    macro_rules! end {
306      () => {
307        if has_declarations {
308          dest.dedent();
309          dest.newline()?;
310          dest.write_char('}')?;
311        }
312      };
313    }
314
315    // Write nested rules after the parent.
316    if supports_nesting {
317      newline!();
318      self.rules.to_css(dest)?;
319      end!();
320    } else {
321      end!();
322      newline!();
323      dest.with_context(&self.selectors, |dest| self.rules.to_css(dest))?;
324    }
325
326    Ok(())
327  }
328}