lightningcss/
printer.rs

1//! CSS serialization and source map generation.
2
3use crate::css_modules::CssModule;
4use crate::dependencies::{Dependency, DependencyOptions};
5use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind};
6use crate::rules::{Location, StyleContext};
7use crate::selector::SelectorList;
8use crate::targets::Targets;
9use crate::vendor_prefix::VendorPrefix;
10use cssparser::{serialize_identifier, serialize_name};
11#[cfg(feature = "sourcemap")]
12use parcel_sourcemap::{OriginalLocation, SourceMap};
13
14/// Options that control how CSS is serialized to a string.
15#[derive(Default)]
16pub struct PrinterOptions<'a> {
17  /// Whether to minify the CSS, i.e. remove white space.
18  pub minify: bool,
19  /// An optional reference to a source map to write mappings into.
20  #[cfg(feature = "sourcemap")]
21  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
22  pub source_map: Option<&'a mut SourceMap>,
23  /// An optional project root path, used to generate relative paths for sources used in CSS module hashes.
24  pub project_root: Option<&'a str>,
25  /// Targets to output the CSS for.
26  pub targets: Targets,
27  /// Whether to analyze dependencies (i.e. `@import` and `url()`).
28  /// If true, the dependencies are returned as part of the
29  /// [ToCssResult](super::stylesheet::ToCssResult).
30  ///
31  /// When enabled, `@import` and `url()` dependencies
32  /// are replaced with hashed placeholders that can be replaced with the final
33  /// urls later (after bundling).
34  pub analyze_dependencies: Option<DependencyOptions>,
35  /// A mapping of pseudo classes to replace with class names that can be applied
36  /// from JavaScript. Useful for polyfills, for example.
37  pub pseudo_classes: Option<PseudoClasses<'a>>,
38}
39
40/// A mapping of user action pseudo classes to replace with class names.
41///
42/// See [PrinterOptions](PrinterOptions).
43#[derive(Default, Debug)]
44pub struct PseudoClasses<'a> {
45  /// The class name to replace `:hover` with.
46  pub hover: Option<&'a str>,
47  /// The class name to replace `:active` with.
48  pub active: Option<&'a str>,
49  /// The class name to replace `:focus` with.
50  pub focus: Option<&'a str>,
51  /// The class name to replace `:focus-visible` with.
52  pub focus_visible: Option<&'a str>,
53  /// The class name to replace `:focus-within` with.
54  pub focus_within: Option<&'a str>,
55}
56
57/// A `Printer` represents a destination to output serialized CSS, as used in
58/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that
59/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String).
60///
61/// A `Printer` keeps track of the current line and column position, and uses
62/// this to generate a source map if provided in the options.
63///
64/// `Printer` also includes helper functions that assist with writing output
65/// that respects options such as `minify`, and `css_modules`.
66pub struct Printer<'a, 'b, 'c, W> {
67  pub(crate) sources: Option<&'c Vec<String>>,
68  dest: &'a mut W,
69  #[cfg(feature = "sourcemap")]
70  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
71  pub(crate) source_map: Option<&'a mut SourceMap>,
72  #[cfg(feature = "sourcemap")]
73  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
74  pub(crate) source_maps: Vec<Option<SourceMap>>,
75  pub(crate) loc: Location,
76  indent: u8,
77  line: u32,
78  col: u32,
79  pub(crate) minify: bool,
80  pub(crate) targets: Targets,
81  /// Vendor prefix override. When non-empty, it overrides
82  /// the vendor prefix of whatever is being printed.
83  pub(crate) vendor_prefix: VendorPrefix,
84  pub(crate) in_calc: bool,
85  pub(crate) css_module: Option<CssModule<'a, 'b, 'c>>,
86  pub(crate) dependencies: Option<Vec<Dependency>>,
87  pub(crate) remove_imports: bool,
88  pub(crate) pseudo_classes: Option<PseudoClasses<'a>>,
89  context: Option<&'a StyleContext<'a, 'b>>,
90}
91
92impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> {
93  /// Create a new Printer wrapping the given destination.
94  pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Self {
95    Printer {
96      sources: None,
97      dest,
98      #[cfg(feature = "sourcemap")]
99      source_map: options.source_map,
100      #[cfg(feature = "sourcemap")]
101      source_maps: Vec::new(),
102      loc: Location {
103        source_index: 0,
104        line: 0,
105        column: 1,
106      },
107      indent: 0,
108      line: 0,
109      col: 0,
110      minify: options.minify,
111      targets: options.targets,
112      vendor_prefix: VendorPrefix::empty(),
113      in_calc: false,
114      css_module: None,
115      dependencies: if options.analyze_dependencies.is_some() {
116        Some(Vec::new())
117      } else {
118        None
119      },
120      remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports),
121      pseudo_classes: options.pseudo_classes,
122      context: None,
123    }
124  }
125
126  /// Returns the current source filename that is being printed.
127  pub fn filename(&self) -> &'c str {
128    if let Some(sources) = self.sources {
129      if let Some(f) = sources.get(self.loc.source_index as usize) {
130        f
131      } else {
132        "unknown.css"
133      }
134    } else {
135      "unknown.css"
136    }
137  }
138
139  /// Writes a raw string to the underlying destination.
140  ///
141  /// NOTE: Is is assumed that the string does not contain any newline characters.
142  /// If such a string is written, it will break source maps.
143  pub fn write_str(&mut self, s: &str) -> Result<(), PrinterError> {
144    self.col += s.len() as u32;
145    self.dest.write_str(s)?;
146    Ok(())
147  }
148
149  /// Write a single character to the underlying destination.
150  pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> {
151    if c == '\n' {
152      self.line += 1;
153      self.col = 0;
154    } else {
155      self.col += 1;
156    }
157    self.dest.write_char(c)?;
158    Ok(())
159  }
160
161  /// Writes a single whitespace character, unless the `minify` option is enabled.
162  ///
163  /// Use `write_char` instead if you wish to force a space character to be written,
164  /// regardless of the `minify` option.
165  pub fn whitespace(&mut self) -> Result<(), PrinterError> {
166    if self.minify {
167      return Ok(());
168    }
169
170    self.write_char(' ')
171  }
172
173  /// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
174  /// If `ws_before` is true, then whitespace is also written before the delimiter.
175  pub fn delim(&mut self, delim: char, ws_before: bool) -> Result<(), PrinterError> {
176    if ws_before {
177      self.whitespace()?;
178    }
179    self.write_char(delim)?;
180    self.whitespace()
181  }
182
183  /// Writes a newline character followed by indentation.
184  /// If the `minify` option is enabled, then nothing is printed.
185  pub fn newline(&mut self) -> Result<(), PrinterError> {
186    if self.minify {
187      return Ok(());
188    }
189
190    self.write_char('\n')?;
191    if self.indent > 0 {
192      self.write_str(&" ".repeat(self.indent as usize))?;
193    }
194
195    Ok(())
196  }
197
198  /// Increases the current indent level.
199  pub fn indent(&mut self) {
200    self.indent += 2;
201  }
202
203  /// Decreases the current indent level.
204  pub fn dedent(&mut self) {
205    self.indent -= 2;
206  }
207
208  /// Increases the current indent level by the given number of characters.
209  pub fn indent_by(&mut self, amt: u8) {
210    self.indent += amt;
211  }
212
213  /// Decreases the current indent level by the given number of characters.
214  pub fn dedent_by(&mut self, amt: u8) {
215    self.indent -= amt;
216  }
217
218  /// Returns whether the indent level is greater than one.
219  pub fn is_nested(&self) -> bool {
220    self.indent > 2
221  }
222
223  /// Adds a mapping to the source map, if any.
224  #[cfg(feature = "sourcemap")]
225  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
226  pub fn add_mapping(&mut self, loc: Location) {
227    self.loc = loc;
228
229    if let Some(map) = &mut self.source_map {
230      let mut original = OriginalLocation {
231        original_line: loc.line,
232        original_column: loc.column - 1,
233        source: loc.source_index,
234        name: None,
235      };
236
237      // Remap using input source map if possible.
238      if let Some(Some(sm)) = self.source_maps.get_mut(loc.source_index as usize) {
239        let mut found_mapping = false;
240        if let Some(mapping) = sm.find_closest_mapping(loc.line, loc.column - 1) {
241          if let Some(orig) = mapping.original {
242            let sources_len = map.get_sources().len();
243            let source_index = map.add_source(sm.get_source(orig.source).unwrap());
244            original.original_line = orig.original_line;
245            original.original_column = orig.original_column;
246            original.source = source_index;
247            original.name = orig.name;
248
249            if map.get_sources().len() > sources_len {
250              let content = sm.get_source_content(orig.source).unwrap().to_owned();
251              let _ = map.set_source_content(source_index as usize, &content);
252            }
253
254            found_mapping = true;
255          }
256        }
257
258        if !found_mapping {
259          return;
260        }
261      }
262
263      map.add_mapping(self.line, self.col, Some(original))
264    }
265  }
266
267  /// Writes a CSS identifier to the underlying destination, escaping it
268  /// as appropriate. If the `css_modules` option was enabled, then a hash
269  /// is added, and the mapping is added to the CSS module.
270  pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> {
271    if handle_css_module {
272      if let Some(css_module) = &mut self.css_module {
273        let dest = &mut self.dest;
274        let mut first = true;
275        css_module.config.pattern.write(
276          &css_module.hashes[self.loc.source_index as usize],
277          &css_module.sources[self.loc.source_index as usize],
278          ident,
279          if let Some(content_hashes) = &css_module.content_hashes {
280            &content_hashes[self.loc.source_index as usize]
281          } else {
282            ""
283          },
284          |s| {
285            self.col += s.len() as u32;
286            if first {
287              first = false;
288              serialize_identifier(s, dest)
289            } else {
290              serialize_name(s, dest)
291            }
292          },
293        )?;
294
295        css_module.add_local(&ident, &ident, self.loc.source_index);
296        return Ok(());
297      }
298    }
299
300    serialize_identifier(ident, self)?;
301    Ok(())
302  }
303
304  pub(crate) fn write_dashed_ident(&mut self, ident: &str, is_declaration: bool) -> Result<(), PrinterError> {
305    self.write_str("--")?;
306
307    match &mut self.css_module {
308      Some(css_module) if css_module.config.dashed_idents => {
309        let dest = &mut self.dest;
310        css_module.config.pattern.write(
311          &css_module.hashes[self.loc.source_index as usize],
312          &css_module.sources[self.loc.source_index as usize],
313          &ident[2..],
314          if let Some(content_hashes) = &css_module.content_hashes {
315            &content_hashes[self.loc.source_index as usize]
316          } else {
317            ""
318          },
319          |s| {
320            self.col += s.len() as u32;
321            serialize_name(s, dest)
322          },
323        )?;
324
325        if is_declaration {
326          css_module.add_dashed(ident, self.loc.source_index);
327        }
328      }
329      _ => {
330        serialize_name(&ident[2..], self)?;
331      }
332    }
333
334    Ok(())
335  }
336
337  /// Returns an error of the given kind at the provided location in the current source file.
338  pub fn error(&self, kind: PrinterErrorKind, loc: crate::dependencies::Location) -> Error<PrinterErrorKind> {
339    Error {
340      kind,
341      loc: Some(ErrorLocation {
342        filename: self.filename().into(),
343        line: loc.line - 1,
344        column: loc.column,
345      }),
346    }
347  }
348
349  pub(crate) fn with_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
350    &mut self,
351    selectors: &SelectorList,
352    f: F,
353  ) -> Result<T, U> {
354    let parent = std::mem::take(&mut self.context);
355    let ctx = StyleContext {
356      selectors: unsafe { std::mem::transmute(selectors) },
357      parent,
358    };
359
360    // I can't figure out what lifetime to use here to convince the compiler that
361    // the reference doesn't live beyond the function call.
362    self.context = Some(unsafe { std::mem::transmute(&ctx) });
363    let res = f(self);
364    self.context = parent;
365    res
366  }
367
368  pub(crate) fn with_cleared_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
369    &mut self,
370    f: F,
371  ) -> Result<T, U> {
372    let parent = std::mem::take(&mut self.context);
373    let res = f(self);
374    self.context = parent;
375    res
376  }
377
378  pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> {
379    self.context.clone()
380  }
381}
382
383impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> {
384  fn write_str(&mut self, s: &str) -> std::fmt::Result {
385    self.col += s.len() as u32;
386    self.dest.write_str(s)
387  }
388}