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  #[inline]
94  fn mapping_columns(&self) -> bool {
95    #[cfg(feature = "sourcemap")]
96    if self.source_map.is_some() {
97      return true;
98    }
99    false
100  }
101
102  #[inline]
103  fn string_columns(&self, text: &str) -> u32 {
104    if self.mapping_columns() {
105      text.encode_utf16().count() as u32
106    } else {
107      text.len() as u32
108    }
109  }
110
111  /// Create a new Printer wrapping the given destination.
112  pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Self {
113    Printer {
114      sources: None,
115      dest,
116      #[cfg(feature = "sourcemap")]
117      source_map: options.source_map,
118      #[cfg(feature = "sourcemap")]
119      source_maps: Vec::new(),
120      loc: Location {
121        source_index: 0,
122        line: 0,
123        column: 1,
124      },
125      indent: 0,
126      line: 0,
127      col: 0,
128      minify: options.minify,
129      targets: TargetsWithSupportsScope::new(options.targets),
130      vendor_prefix: VendorPrefix::empty(),
131      in_calc: false,
132      css_module: None,
133      dependencies: if options.analyze_dependencies.is_some() {
134        Some(Vec::new())
135      } else {
136        None
137      },
138      remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports),
139      pseudo_classes: options.pseudo_classes,
140      context: None,
141    }
142  }
143
144  /// Returns the current source filename that is being printed.
145  pub fn filename(&self) -> &'c str {
146    if let Some(sources) = self.sources {
147      if let Some(f) = sources.get(self.loc.source_index as usize) {
148        f
149      } else {
150        "unknown.css"
151      }
152    } else {
153      "unknown.css"
154    }
155  }
156
157  /// Writes a raw string to the underlying destination.
158  ///
159  /// NOTE: Is is assumed that the string does not contain any newline characters.
160  /// If such a string is written, it will break source maps.
161  pub fn write_str(&mut self, s: &str) -> Result<(), PrinterError> {
162    self.col += self.string_columns(s);
163    self.dest.write_str(s)?;
164    Ok(())
165  }
166
167  /// Writes a raw string which may contain newlines to the underlying destination.
168  pub fn write_str_with_newlines(&mut self, s: &str) -> Result<(), PrinterError> {
169    let mut last_line_start: usize = 0;
170
171    for (idx, n) in s.char_indices() {
172      if n == '\n' {
173        self.line += 1;
174        self.col = 0;
175
176        // Keep track of where the *next* line starts
177        last_line_start = idx + 1;
178      }
179    }
180
181    self.col += self.string_columns(&s[last_line_start..]);
182    self.dest.write_str(s)?;
183    Ok(())
184  }
185
186  /// Write a single character to the underlying destination.
187  pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> {
188    if c == '\n' {
189      self.line += 1;
190      self.col = 0;
191    } else {
192      self.col += if self.mapping_columns() {
193        c.len_utf16() as u32
194      } else {
195        1
196      };
197    }
198    self.dest.write_char(c)?;
199    Ok(())
200  }
201
202  /// Writes a single whitespace character, unless the `minify` option is enabled.
203  ///
204  /// Use `write_char` instead if you wish to force a space character to be written,
205  /// regardless of the `minify` option.
206  pub fn whitespace(&mut self) -> Result<(), PrinterError> {
207    if self.minify {
208      return Ok(());
209    }
210
211    self.write_char(' ')
212  }
213
214  /// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
215  /// If `ws_before` is true, then whitespace is also written before the delimiter.
216  pub fn delim(&mut self, delim: char, ws_before: bool) -> Result<(), PrinterError> {
217    if ws_before {
218      self.whitespace()?;
219    }
220    self.write_char(delim)?;
221    self.whitespace()
222  }
223
224  /// Writes a newline character followed by indentation.
225  /// If the `minify` option is enabled, then nothing is printed.
226  pub fn newline(&mut self) -> Result<(), PrinterError> {
227    if self.minify {
228      return Ok(());
229    }
230
231    self.write_char('\n')?;
232    if self.indent > 0 {
233      self.write_str(&" ".repeat(self.indent as usize))?;
234    }
235
236    Ok(())
237  }
238
239  /// Increases the current indent level.
240  pub fn indent(&mut self) {
241    self.indent += 2;
242  }
243
244  /// Decreases the current indent level.
245  pub fn dedent(&mut self) {
246    self.indent -= 2;
247  }
248
249  /// Increases the current indent level by the given number of characters.
250  pub fn indent_by(&mut self, amt: u8) {
251    self.indent += amt;
252  }
253
254  /// Decreases the current indent level by the given number of characters.
255  pub fn dedent_by(&mut self, amt: u8) {
256    self.indent -= amt;
257  }
258
259  /// Returns whether the indent level is greater than one.
260  pub fn is_nested(&self) -> bool {
261    self.indent > 2
262  }
263
264  /// Adds a mapping to the source map, if any.
265  #[cfg(feature = "sourcemap")]
266  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
267  pub fn add_mapping(&mut self, loc: Location) {
268    self.loc = loc;
269
270    if let Some(map) = &mut self.source_map {
271      let mut original = OriginalLocation {
272        original_line: loc.line,
273        original_column: loc.column - 1,
274        source: loc.source_index,
275        name: None,
276      };
277
278      // Remap using input source map if possible.
279      if let Some(Some(sm)) = self.source_maps.get_mut(loc.source_index as usize) {
280        let mut found_mapping = false;
281        if let Some(mapping) = sm.find_closest_mapping(loc.line, loc.column - 1) {
282          if let Some(orig) = mapping.original {
283            let sources_len = map.get_sources().len();
284            let source_index = map.add_source(sm.get_source(orig.source).unwrap());
285            let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap()));
286            original.original_line = orig.original_line;
287            original.original_column = orig.original_column;
288            original.source = source_index;
289            original.name = name;
290
291            if map.get_sources().len() > sources_len {
292              let content = sm.get_source_content(orig.source).unwrap().to_owned();
293              let _ = map.set_source_content(source_index as usize, &content);
294            }
295
296            found_mapping = true;
297          }
298        }
299
300        if !found_mapping {
301          return;
302        }
303      }
304
305      map.add_mapping(self.line, self.col, Some(original))
306    }
307  }
308
309  /// Writes a CSS identifier to the underlying destination, escaping it
310  /// as appropriate. If the `css_modules` option was enabled, then a hash
311  /// is added, and the mapping is added to the CSS module.
312  pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> {
313    let mapping_columns = self.mapping_columns();
314    if handle_css_module {
315      if let Some(css_module) = &mut self.css_module {
316        let dest = &mut self.dest;
317        let mut first = true;
318        css_module.config.pattern.write(
319          &css_module.hashes[self.loc.source_index as usize],
320          &css_module.sources[self.loc.source_index as usize],
321          ident,
322          if let Some(content_hashes) = &css_module.content_hashes {
323            &content_hashes[self.loc.source_index as usize]
324          } else {
325            ""
326          },
327          |s| {
328            if mapping_columns {
329              let mut writer = MappingWriter {
330                dest: &mut **dest,
331                column: &mut self.col,
332              };
333              if first {
334                first = false;
335                return serialize_identifier(s, &mut writer);
336              }
337              return serialize_name(s, &mut writer);
338            }
339            self.col += s.len() as u32;
340            if first {
341              first = false;
342              serialize_identifier(s, dest)
343            } else {
344              serialize_name(s, dest)
345            }
346          },
347        )?;
348
349        css_module.add_local(&ident, &ident, self.loc.source_index);
350        return Ok(());
351      }
352    }
353
354    serialize_identifier(ident, self)?;
355    Ok(())
356  }
357
358  pub(crate) fn write_dashed_ident(&mut self, ident: &str, is_declaration: bool) -> Result<(), PrinterError> {
359    self.write_str("--")?;
360    let mapping_columns = self.mapping_columns();
361
362    match &mut self.css_module {
363      Some(css_module) if css_module.config.dashed_idents => {
364        let dest = &mut self.dest;
365        css_module.config.pattern.write(
366          &css_module.hashes[self.loc.source_index as usize],
367          &css_module.sources[self.loc.source_index as usize],
368          &ident[2..],
369          if let Some(content_hashes) = &css_module.content_hashes {
370            &content_hashes[self.loc.source_index as usize]
371          } else {
372            ""
373          },
374          |s| {
375            if mapping_columns {
376              return serialize_name(
377                s,
378                &mut MappingWriter {
379                  dest: &mut **dest,
380                  column: &mut self.col,
381                },
382              );
383            }
384            self.col += s.len() as u32;
385            serialize_name(s, dest)
386          },
387        )?;
388
389        if is_declaration {
390          css_module.add_dashed(ident, self.loc.source_index);
391        }
392      }
393      _ => {
394        serialize_name(&ident[2..], self)?;
395      }
396    }
397
398    Ok(())
399  }
400
401  /// Returns an error of the given kind at the provided location in the current source file.
402  pub fn error(&self, kind: PrinterErrorKind, loc: crate::dependencies::Location) -> Error<PrinterErrorKind> {
403    Error {
404      kind,
405      loc: Some(ErrorLocation {
406        filename: self.filename().into(),
407        line: loc.line - 1,
408        column: loc.column,
409      }),
410    }
411  }
412
413  pub(crate) fn with_context<T, U, F: FnOnce(&mut Printer<'a, 'c, W>) -> Result<T, U>>(
414    &mut self,
415    selectors: &SelectorList,
416    f: F,
417  ) -> Result<T, U> {
418    let parent = std::mem::take(&mut self.context);
419    let ctx = StyleContext {
420      selectors: unsafe { std::mem::transmute(selectors) },
421      parent,
422    };
423
424    // I can't figure out what lifetime to use here to convince the compiler that
425    // the reference doesn't live beyond the function call.
426    self.context = Some(unsafe { std::mem::transmute(&ctx) });
427    let res = f(self);
428    self.context = parent;
429    res
430  }
431
432  pub(crate) fn with_cleared_context<T, U, F: FnOnce(&mut Printer<'a, 'c, W>) -> Result<T, U>>(
433    &mut self,
434    f: F,
435  ) -> Result<T, U> {
436    let parent = std::mem::take(&mut self.context);
437    let res = f(self);
438    self.context = parent;
439    res
440  }
441
442  pub(crate) fn with_parent_context<T, U, F: FnOnce(&mut Printer<'a, 'c, W>) -> Result<T, U>>(
443    &mut self,
444    f: F,
445  ) -> Result<T, U> {
446    let parent = std::mem::take(&mut self.context);
447    if let Some(parent) = parent {
448      self.context = parent.parent;
449    }
450    let res = f(self);
451    self.context = parent;
452    res
453  }
454
455  pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'c>> {
456    self.context.clone()
457  }
458}
459
460impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'c, W> {
461  fn write_str(&mut self, s: &str) -> std::fmt::Result {
462    self.col += self.string_columns(s);
463    self.dest.write_str(s)
464  }
465}
466
467// CSS Modules serialize directly into the destination. Count the escaped
468// bytes actually written, rather than the unescaped pattern input length.
469struct MappingWriter<'a, W> {
470  dest: &'a mut W,
471  column: &'a mut u32,
472}
473
474impl<W: std::fmt::Write> std::fmt::Write for MappingWriter<'_, W> {
475  fn write_str(&mut self, text: &str) -> std::fmt::Result {
476    *self.column += text.encode_utf16().count() as u32;
477    self.dest.write_str(text)
478  }
479}