Skip to main content

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, TargetsWithSupportsScope};
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, '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: TargetsWithSupportsScope,
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, '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, 'c>>,
90}
91
92impl<'a, 'c, W: std::fmt::Write + Sized> Printer<'a, '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: TargetsWithSupportsScope::new(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  /// Writes a raw string which may contain newlines to the underlying destination.
150  pub fn write_str_with_newlines(&mut self, s: &str) -> Result<(), PrinterError> {
151    let mut last_line_start: usize = 0;
152
153    for (idx, n) in s.char_indices() {
154      if n == '\n' {
155        self.line += 1;
156        self.col = 0;
157
158        // Keep track of where the *next* line starts
159        last_line_start = idx + 1;
160      }
161    }
162
163    self.col += (s.len() - last_line_start) as u32;
164    self.dest.write_str(s)?;
165    Ok(())
166  }
167
168  /// Write a single character to the underlying destination.
169  pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> {
170    if c == '\n' {
171      self.line += 1;
172      self.col = 0;
173    } else {
174      self.col += 1;
175    }
176    self.dest.write_char(c)?;
177    Ok(())
178  }
179
180  /// Writes a single whitespace character, unless the `minify` option is enabled.
181  ///
182  /// Use `write_char` instead if you wish to force a space character to be written,
183  /// regardless of the `minify` option.
184  pub fn whitespace(&mut self) -> Result<(), PrinterError> {
185    if self.minify {
186      return Ok(());
187    }
188
189    self.write_char(' ')
190  }
191
192  /// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
193  /// If `ws_before` is true, then whitespace is also written before the delimiter.
194  pub fn delim(&mut self, delim: char, ws_before: bool) -> Result<(), PrinterError> {
195    if ws_before {
196      self.whitespace()?;
197    }
198    self.write_char(delim)?;
199    self.whitespace()
200  }
201
202  /// Writes a newline character followed by indentation.
203  /// If the `minify` option is enabled, then nothing is printed.
204  pub fn newline(&mut self) -> Result<(), PrinterError> {
205    if self.minify {
206      return Ok(());
207    }
208
209    self.write_char('\n')?;
210    if self.indent > 0 {
211      self.write_str(&" ".repeat(self.indent as usize))?;
212    }
213
214    Ok(())
215  }
216
217  /// Increases the current indent level.
218  pub fn indent(&mut self) {
219    self.indent += 2;
220  }
221
222  /// Decreases the current indent level.
223  pub fn dedent(&mut self) {
224    self.indent -= 2;
225  }
226
227  /// Increases the current indent level by the given number of characters.
228  pub fn indent_by(&mut self, amt: u8) {
229    self.indent += amt;
230  }
231
232  /// Decreases the current indent level by the given number of characters.
233  pub fn dedent_by(&mut self, amt: u8) {
234    self.indent -= amt;
235  }
236
237  /// Returns whether the indent level is greater than one.
238  pub fn is_nested(&self) -> bool {
239    self.indent > 2
240  }
241
242  /// Adds a mapping to the source map, if any.
243  #[cfg(feature = "sourcemap")]
244  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
245  pub fn add_mapping(&mut self, loc: Location) {
246    self.loc = loc;
247
248    if let Some(map) = &mut self.source_map {
249      let mut original = OriginalLocation {
250        original_line: loc.line,
251        original_column: loc.column - 1,
252        source: loc.source_index,
253        name: None,
254      };
255
256      // Remap using input source map if possible.
257      if let Some(Some(sm)) = self.source_maps.get_mut(loc.source_index as usize) {
258        let mut found_mapping = false;
259        if let Some(mapping) = sm.find_closest_mapping(loc.line, loc.column - 1) {
260          if let Some(orig) = mapping.original {
261            let sources_len = map.get_sources().len();
262            let source_index = map.add_source(sm.get_source(orig.source).unwrap());
263            let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap()));
264            original.original_line = orig.original_line;
265            original.original_column = orig.original_column;
266            original.source = source_index;
267            original.name = name;
268
269            if map.get_sources().len() > sources_len {
270              let content = sm.get_source_content(orig.source).unwrap().to_owned();
271              let _ = map.set_source_content(source_index as usize, &content);
272            }
273
274            found_mapping = true;
275          }
276        }
277
278        if !found_mapping {
279          return;
280        }
281      }
282
283      map.add_mapping(self.line, self.col, Some(original))
284    }
285  }
286
287  /// Writes a CSS identifier to the underlying destination, escaping it
288  /// as appropriate. If the `css_modules` option was enabled, then a hash
289  /// is added, and the mapping is added to the CSS module.
290  pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> {
291    if handle_css_module {
292      if let Some(css_module) = &mut self.css_module {
293        let dest = &mut self.dest;
294        let mut first = true;
295        css_module.config.pattern.write(
296          &css_module.hashes[self.loc.source_index as usize],
297          &css_module.sources[self.loc.source_index as usize],
298          ident,
299          if let Some(content_hashes) = &css_module.content_hashes {
300            &content_hashes[self.loc.source_index as usize]
301          } else {
302            ""
303          },
304          |s| {
305            self.col += s.len() as u32;
306            if first {
307              first = false;
308              serialize_identifier(s, dest)
309            } else {
310              serialize_name(s, dest)
311            }
312          },
313        )?;
314
315        css_module.add_local(&ident, &ident, self.loc.source_index);
316        return Ok(());
317      }
318    }
319
320    serialize_identifier(ident, self)?;
321    Ok(())
322  }
323
324  pub(crate) fn write_dashed_ident(&mut self, ident: &str, is_declaration: bool) -> Result<(), PrinterError> {
325    self.write_str("--")?;
326
327    match &mut self.css_module {
328      Some(css_module) if css_module.config.dashed_idents => {
329        let dest = &mut self.dest;
330        css_module.config.pattern.write(
331          &css_module.hashes[self.loc.source_index as usize],
332          &css_module.sources[self.loc.source_index as usize],
333          &ident[2..],
334          if let Some(content_hashes) = &css_module.content_hashes {
335            &content_hashes[self.loc.source_index as usize]
336          } else {
337            ""
338          },
339          |s| {
340            self.col += s.len() as u32;
341            serialize_name(s, dest)
342          },
343        )?;
344
345        if is_declaration {
346          css_module.add_dashed(ident, self.loc.source_index);
347        }
348      }
349      _ => {
350        serialize_name(&ident[2..], self)?;
351      }
352    }
353
354    Ok(())
355  }
356
357  /// Returns an error of the given kind at the provided location in the current source file.
358  pub fn error(&self, kind: PrinterErrorKind, loc: crate::dependencies::Location) -> Error<PrinterErrorKind> {
359    Error {
360      kind,
361      loc: Some(ErrorLocation {
362        filename: self.filename().into(),
363        line: loc.line - 1,
364        column: loc.column,
365      }),
366    }
367  }
368
369  pub(crate) fn with_context<T, U, F: FnOnce(&mut Printer<'a, 'c, W>) -> Result<T, U>>(
370    &mut self,
371    selectors: &SelectorList,
372    f: F,
373  ) -> Result<T, U> {
374    let parent = std::mem::take(&mut self.context);
375    let ctx = StyleContext {
376      selectors: unsafe { std::mem::transmute(selectors) },
377      parent,
378    };
379
380    // I can't figure out what lifetime to use here to convince the compiler that
381    // the reference doesn't live beyond the function call.
382    self.context = Some(unsafe { std::mem::transmute(&ctx) });
383    let res = f(self);
384    self.context = parent;
385    res
386  }
387
388  pub(crate) fn with_cleared_context<T, U, F: FnOnce(&mut Printer<'a, 'c, W>) -> Result<T, U>>(
389    &mut self,
390    f: F,
391  ) -> Result<T, U> {
392    let parent = std::mem::take(&mut self.context);
393    let res = f(self);
394    self.context = parent;
395    res
396  }
397
398  pub(crate) fn with_parent_context<T, U, F: FnOnce(&mut Printer<'a, 'c, W>) -> Result<T, U>>(
399    &mut self,
400    f: F,
401  ) -> Result<T, U> {
402    let parent = std::mem::take(&mut self.context);
403    if let Some(parent) = parent {
404      self.context = parent.parent;
405    }
406    let res = f(self);
407    self.context = parent;
408    res
409  }
410
411  pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'c>> {
412    self.context.clone()
413  }
414}
415
416impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'c, W> {
417  fn write_str(&mut self, s: &str) -> std::fmt::Result {
418    self.col += s.len() as u32;
419    self.dest.write_str(s)
420  }
421}