lightningcss/
stylesheet.rs

1//! CSS style sheets and style attributes.
2//!
3//! A [StyleSheet](StyleSheet) represents a `.css` file or `<style>` element in HTML.
4//! A [StyleAttribute](StyleAttribute) represents an inline `style` attribute in HTML.
5
6use crate::context::{DeclarationContext, PropertyHandlerContext};
7use crate::css_modules::{hash, CssModule, CssModuleExports, CssModuleReferences};
8use crate::declaration::{DeclarationBlock, DeclarationHandler};
9use crate::dependencies::Dependency;
10use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind};
11use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser};
12use crate::printer::Printer;
13use crate::rules::{CssRule, CssRuleList, MinifyContext};
14use crate::targets::{should_compile, Targets, TargetsWithSupportsScope};
15use crate::traits::{AtRuleParser, ToCss};
16use crate::values::string::CowArcStr;
17#[cfg(feature = "visitor")]
18use crate::visitor::{Visit, VisitTypes, Visitor};
19use cssparser::{Parser, ParserInput, StyleSheetParser};
20#[cfg(feature = "sourcemap")]
21use parcel_sourcemap::SourceMap;
22use std::collections::{HashMap, HashSet};
23
24pub use crate::parser::{ParserFlags, ParserOptions};
25pub use crate::printer::PrinterOptions;
26pub use crate::printer::PseudoClasses;
27
28/// A CSS style sheet, representing a `.css` file or inline `<style>` element.
29///
30/// Style sheets can be parsed from a string, constructed from scratch,
31/// or created using a [Bundler](super::bundler::Bundler). Then, they can be
32/// minified and transformed for a set of target browsers, and serialied to a string.
33///
34/// # Example
35///
36/// ```
37/// use lightningcss::stylesheet::{
38///   StyleSheet, ParserOptions, MinifyOptions, PrinterOptions
39/// };
40///
41/// // Parse a style sheet from a string.
42/// let mut stylesheet = StyleSheet::parse(
43///   r#"
44///   .foo {
45///     color: red;
46///   }
47///
48///   .bar {
49///     color: red;
50///   }
51///   "#,
52///   ParserOptions::default()
53/// ).unwrap();
54///
55/// // Minify the stylesheet.
56/// stylesheet.minify(MinifyOptions::default()).unwrap();
57///
58/// // Serialize it to a string.
59/// let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
60/// assert_eq!(res.code, ".foo, .bar {\n  color: red;\n}\n");
61/// ```
62#[derive(Debug)]
63#[cfg_attr(
64  feature = "serde",
65  derive(serde::Serialize, serde::Deserialize),
66  serde(rename_all = "camelCase")
67)]
68#[cfg_attr(
69  feature = "jsonschema",
70  derive(schemars::JsonSchema),
71  schemars(rename = "StyleSheet", bound = "T: schemars::JsonSchema")
72)]
73pub struct StyleSheet<'i, 'o, T = DefaultAtRule> {
74  /// A list of top-level rules within the style sheet.
75  #[cfg_attr(feature = "serde", serde(borrow))]
76  pub rules: CssRuleList<'i, T>,
77  /// A list of file names for all source files included within the style sheet.
78  /// Sources are referenced by index in the `loc` property of each rule.
79  pub sources: Vec<String>,
80  /// The source map URL extracted from the original style sheet.
81  pub(crate) source_map_urls: Vec<Option<String>>,
82  /// The license comments that appeared at the start of the file.
83  pub license_comments: Vec<CowArcStr<'i>>,
84  /// A list of content hashes for all source files included within the style sheet.
85  /// This is only set if CSS modules are enabled and the pattern includes [content-hash].
86  #[cfg_attr(feature = "serde", serde(skip))]
87  pub(crate) content_hashes: Option<Vec<String>>,
88  #[cfg_attr(feature = "serde", serde(skip))]
89  /// The options the style sheet was originally parsed with.
90  options: ParserOptions<'o, 'i>,
91}
92
93/// Options for the `minify` function of a [StyleSheet](StyleSheet)
94/// or [StyleAttribute](StyleAttribute).
95#[derive(Default)]
96pub struct MinifyOptions {
97  /// Targets to compile the CSS for.
98  pub targets: Targets,
99  /// A list of known unused symbols, including CSS class names,
100  /// ids, and `@keyframe` names. The declarations of these will be removed.
101  pub unused_symbols: HashSet<String>,
102}
103
104/// A result returned from `to_css`, including the serialize CSS
105/// and other metadata depending on the input options.
106#[derive(Debug)]
107pub struct ToCssResult {
108  /// Serialized CSS code.
109  pub code: String,
110  /// A map of CSS module exports, if the `css_modules` option was
111  /// enabled during parsing.
112  pub exports: Option<CssModuleExports>,
113  /// A map of CSS module references, if the `css_modules` config
114  /// had `dashed_idents` enabled.
115  pub references: Option<CssModuleReferences>,
116  /// A list of dependencies (e.g. `@import` or `url()`) found in
117  /// the style sheet, if the `analyze_dependencies` option is enabled.
118  pub dependencies: Option<Vec<Dependency>>,
119}
120
121impl<'i, 'o> StyleSheet<'i, 'o, DefaultAtRule> {
122  /// Parse a style sheet from a string.
123  pub fn parse(code: &'i str, options: ParserOptions<'o, 'i>) -> Result<Self, Error<ParserError<'i>>> {
124    Self::parse_with(code, options, &mut DefaultAtRuleParser)
125  }
126}
127
128impl<'i, 'o, T> StyleSheet<'i, 'o, T>
129where
130  T: ToCss + Clone,
131{
132  /// Creates a new style sheet with the given source filenames and rules.
133  pub fn new(
134    sources: Vec<String>,
135    rules: CssRuleList<'i, T>,
136    options: ParserOptions<'o, 'i>,
137  ) -> StyleSheet<'i, 'o, T> {
138    StyleSheet {
139      sources,
140      source_map_urls: Vec::new(),
141      license_comments: Vec::new(),
142      content_hashes: None,
143      rules,
144      options,
145    }
146  }
147
148  /// Parse a style sheet from a string.
149  pub fn parse_with<P: AtRuleParser<'i, AtRule = T>>(
150    code: &'i str,
151    mut options: ParserOptions<'o, 'i>,
152    at_rule_parser: &mut P,
153  ) -> Result<Self, Error<ParserError<'i>>> {
154    let mut input = ParserInput::new(&code);
155    let mut parser = Parser::new(&mut input);
156    let mut license_comments = Vec::new();
157
158    let mut content_hashes = None;
159    if let Some(config) = &options.css_modules {
160      if config.pattern.has_content_hash() {
161        content_hashes = Some(vec![hash(
162          &code,
163          matches!(config.pattern.segments[0], crate::css_modules::Segment::ContentHash),
164        )]);
165      }
166    }
167
168    let mut state = parser.state();
169    while let Ok(token) = parser.next_including_whitespace_and_comments() {
170      match token {
171        cssparser::Token::WhiteSpace(..) => {}
172        cssparser::Token::Comment(comment) if comment.starts_with('!') => {
173          license_comments.push((*comment).into());
174        }
175        cssparser::Token::Comment(comment) if comment.contains("cssmodules-pure-no-check") => {
176          if let Some(css_modules) = &mut options.css_modules {
177            css_modules.pure = false;
178          }
179        }
180        _ => break,
181      }
182      state = parser.state();
183    }
184    parser.reset(&state);
185
186    let mut rules = CssRuleList(vec![]);
187    let mut rule_parser = TopLevelRuleParser::new(&mut options, at_rule_parser, &mut rules);
188    let mut rule_list_parser = StyleSheetParser::new(&mut parser, &mut rule_parser);
189
190    while let Some(rule) = rule_list_parser.next() {
191      match rule {
192        Ok(()) => {}
193        Err((e, _)) => {
194          let options = &mut rule_list_parser.parser.options;
195          if options.error_recovery {
196            options.warn(e);
197            continue;
198          }
199
200          return Err(Error::from(e, options.filename.clone()));
201        }
202      }
203    }
204
205    Ok(StyleSheet {
206      sources: vec![options.filename.clone()],
207      source_map_urls: vec![parser.current_source_map_url().map(|s| s.to_owned())],
208      content_hashes,
209      rules,
210      license_comments,
211      options,
212    })
213  }
214
215  /// Returns the source map URL for the source at the given index.
216  pub fn source_map_url(&self, source_index: usize) -> Option<&String> {
217    self.source_map_urls.get(source_index)?.as_ref()
218  }
219
220  /// Returns the inline source map associated with the source at the given index.
221  #[cfg(feature = "sourcemap")]
222  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
223  pub fn source_map(&self, source_index: usize) -> Option<SourceMap> {
224    SourceMap::from_data_url("/", self.source_map_url(source_index)?).ok()
225  }
226
227  /// Minify and transform the style sheet for the provided browser targets.
228  pub fn minify(&mut self, options: MinifyOptions) -> Result<(), Error<MinifyErrorKind>> {
229    let context = PropertyHandlerContext::new(options.targets, &options.unused_symbols);
230    let mut handler = DeclarationHandler::default();
231    let mut important_handler = DeclarationHandler::default();
232
233    // @custom-media rules may be defined after they are referenced, but may only be defined at the top level
234    // of a stylesheet. Do a pre-scan here and create a lookup table by name.
235    let custom_media = if self.options.flags.contains(ParserFlags::CUSTOM_MEDIA)
236      && should_compile!(options.targets, CustomMediaQueries)
237    {
238      let mut custom_media = HashMap::new();
239      for rule in &self.rules.0 {
240        if let CssRule::CustomMedia(rule) = rule {
241          custom_media.insert(rule.name.0.clone(), rule.clone());
242        }
243      }
244      Some(custom_media)
245    } else {
246      None
247    };
248
249    let mut ctx = MinifyContext {
250      targets: TargetsWithSupportsScope::new(options.targets),
251      handler: &mut handler,
252      important_handler: &mut important_handler,
253      handler_context: context,
254      unused_symbols: &options.unused_symbols,
255      custom_media,
256      css_modules: self.options.css_modules.is_some(),
257      pure_css_modules: self.options.css_modules.as_ref().map(|c| c.pure).unwrap_or_default(),
258    };
259
260    self.rules.minify(&mut ctx, false).map_err(|e| Error {
261      kind: e.kind,
262      loc: Some(ErrorLocation::new(
263        e.loc,
264        self.sources[e.loc.source_index as usize].clone(),
265      )),
266    })?;
267
268    Ok(())
269  }
270
271  /// Serialize the style sheet to a CSS string.
272  pub fn to_css(&self, options: PrinterOptions) -> Result<ToCssResult, Error<PrinterErrorKind>> {
273    // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124.
274    let mut dest = String::with_capacity(1);
275    let project_root = options.project_root.clone();
276    let mut printer = Printer::new(&mut dest, options);
277
278    #[cfg(feature = "sourcemap")]
279    {
280      printer.sources = Some(&self.sources);
281    }
282
283    #[cfg(feature = "sourcemap")]
284    if printer.source_map.is_some() {
285      printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map(i)).collect();
286    }
287
288    for comment in &self.license_comments {
289      printer.write_str("/*")?;
290      printer.write_str_with_newlines(comment)?;
291      printer.write_str_with_newlines("*/\n")?;
292    }
293
294    if let Some(config) = &self.options.css_modules {
295      let mut references = HashMap::new();
296      printer.css_module = Some(CssModule::new(
297        config,
298        &self.sources,
299        project_root,
300        &mut references,
301        &self.content_hashes,
302      ));
303
304      self.rules.to_css(&mut printer)?;
305      printer.newline()?;
306
307      Ok(ToCssResult {
308        dependencies: printer.dependencies,
309        exports: Some(std::mem::take(
310          &mut printer.css_module.unwrap().exports_by_source_index[0],
311        )),
312        code: dest,
313        references: Some(references),
314      })
315    } else {
316      self.rules.to_css(&mut printer)?;
317      printer.newline()?;
318
319      Ok(ToCssResult {
320        dependencies: printer.dependencies,
321        code: dest,
322        exports: None,
323        references: None,
324      })
325    }
326  }
327}
328
329#[cfg(feature = "visitor")]
330#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
331impl<'i, 'o, T, V> Visit<'i, T, V> for StyleSheet<'i, 'o, T>
332where
333  T: Visit<'i, T, V>,
334  V: ?Sized + Visitor<'i, T>,
335{
336  const CHILD_TYPES: VisitTypes = VisitTypes::all();
337
338  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
339    visitor.visit_stylesheet(self)
340  }
341
342  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
343    self.rules.visit(visitor)
344  }
345}
346
347/// An inline style attribute, as in HTML or SVG.
348///
349/// Style attributes can be parsed from a string, minified and transformed
350/// for a set of target browsers, and serialied to a string.
351///
352/// # Example
353///
354/// ```
355/// use lightningcss::stylesheet::{
356///   StyleAttribute, ParserOptions, MinifyOptions, PrinterOptions
357/// };
358///
359/// // Parse a style sheet from a string.
360/// let mut style = StyleAttribute::parse(
361///   "color: yellow; font-family: 'Helvetica';",
362///   ParserOptions::default()
363/// ).unwrap();
364///
365/// // Minify the stylesheet.
366/// style.minify(MinifyOptions::default());
367///
368/// // Serialize it to a string.
369/// let res = style.to_css(PrinterOptions::default()).unwrap();
370/// assert_eq!(res.code, "color: #ff0; font-family: Helvetica");
371/// ```
372#[cfg_attr(feature = "visitor", derive(Visit))]
373#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
374pub struct StyleAttribute<'i> {
375  /// The declarations in the style attribute.
376  pub declarations: DeclarationBlock<'i>,
377  #[cfg_attr(feature = "visitor", skip_visit)]
378  sources: Vec<String>,
379}
380
381impl<'i> StyleAttribute<'i> {
382  /// Parses a style attribute from a string.
383  pub fn parse(
384    code: &'i str,
385    options: ParserOptions<'_, 'i>,
386  ) -> Result<StyleAttribute<'i>, Error<ParserError<'i>>> {
387    let mut input = ParserInput::new(&code);
388    let mut parser = Parser::new(&mut input);
389    Ok(StyleAttribute {
390      declarations: DeclarationBlock::parse(&mut parser, &options).map_err(|e| Error::from(e, "".into()))?,
391      sources: vec![options.filename],
392    })
393  }
394
395  /// Minify and transform the style attribute for the provided browser targets.
396  pub fn minify(&mut self, options: MinifyOptions) {
397    let mut context = PropertyHandlerContext::new(options.targets, &options.unused_symbols);
398    let mut handler = DeclarationHandler::default();
399    let mut important_handler = DeclarationHandler::default();
400    context.context = DeclarationContext::StyleAttribute;
401    self.declarations.minify(&mut handler, &mut important_handler, &mut context);
402  }
403
404  /// Serializes the style attribute to a CSS string.
405  pub fn to_css(&self, options: PrinterOptions) -> Result<ToCssResult, PrinterError> {
406    #[cfg(feature = "sourcemap")]
407    assert!(
408      options.source_map.is_none(),
409      "Source maps are not supported for style attributes"
410    );
411
412    // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124.
413    let mut dest = String::with_capacity(1);
414    let mut printer = Printer::new(&mut dest, options);
415    printer.sources = Some(&self.sources);
416
417    self.declarations.to_css(&mut printer)?;
418
419    Ok(ToCssResult {
420      dependencies: printer.dependencies,
421      code: dest,
422      exports: None,
423      references: None,
424    })
425  }
426}