blob: b231fbecc662cf813117c96f28af8678da929c2d [file] [log] [blame]
//! CSS serialization and source map generation.
use crate::css_modules::CssModule;
use crate::dependencies::{Dependency, DependencyOptions};
use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind};
use crate::rules::{Location, StyleContext};
use crate::selector::SelectorList;
use crate::targets::{Targets, TargetsWithSupportsScope};
use crate::vendor_prefix::VendorPrefix;
use cssparser::{serialize_identifier, serialize_name};
#[cfg(feature = "sourcemap")]
use parcel_sourcemap::{OriginalLocation, SourceMap};
/// Options that control how CSS is serialized to a string.
#[derive(Default)]
pub struct PrinterOptions<'a> {
/// Whether to minify the CSS, i.e. remove white space.
pub minify: bool,
/// An optional reference to a source map to write mappings into.
#[cfg(feature = "sourcemap")]
#[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
pub source_map: Option<&'a mut SourceMap>,
/// An optional project root path, used to generate relative paths for sources used in CSS module hashes.
pub project_root: Option<&'a str>,
/// Targets to output the CSS for.
pub targets: Targets,
/// Whether to analyze dependencies (i.e. `@import` and `url()`).
/// If true, the dependencies are returned as part of the
/// [ToCssResult](super::stylesheet::ToCssResult).
///
/// When enabled, `@import` and `url()` dependencies
/// are replaced with hashed placeholders that can be replaced with the final
/// urls later (after bundling).
pub analyze_dependencies: Option<DependencyOptions>,
/// A mapping of pseudo classes to replace with class names that can be applied
/// from JavaScript. Useful for polyfills, for example.
pub pseudo_classes: Option<PseudoClasses<'a>>,
}
/// A mapping of user action pseudo classes to replace with class names.
///
/// See [PrinterOptions](PrinterOptions).
#[derive(Default, Debug)]
pub struct PseudoClasses<'a> {
/// The class name to replace `:hover` with.
pub hover: Option<&'a str>,
/// The class name to replace `:active` with.
pub active: Option<&'a str>,
/// The class name to replace `:focus` with.
pub focus: Option<&'a str>,
/// The class name to replace `:focus-visible` with.
pub focus_visible: Option<&'a str>,
/// The class name to replace `:focus-within` with.
pub focus_within: Option<&'a str>,
}
/// A `Printer` represents a destination to output serialized CSS, as used in
/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that
/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String).
///
/// A `Printer` keeps track of the current line and column position, and uses
/// this to generate a source map if provided in the options.
///
/// `Printer` also includes helper functions that assist with writing output
/// that respects options such as `minify`, and `css_modules`.
pub struct Printer<'a, 'b, 'c, W> {
pub(crate) sources: Option<&'c Vec<String>>,
dest: &'a mut W,
#[cfg(feature = "sourcemap")]
#[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
pub(crate) source_map: Option<&'a mut SourceMap>,
#[cfg(feature = "sourcemap")]
#[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
pub(crate) source_maps: Vec<Option<SourceMap>>,
pub(crate) loc: Location,
indent: u8,
line: u32,
col: u32,
pub(crate) minify: bool,
pub(crate) targets: TargetsWithSupportsScope,
/// Vendor prefix override. When non-empty, it overrides
/// the vendor prefix of whatever is being printed.
pub(crate) vendor_prefix: VendorPrefix,
pub(crate) in_calc: bool,
pub(crate) css_module: Option<CssModule<'a, 'b, 'c>>,
pub(crate) dependencies: Option<Vec<Dependency>>,
pub(crate) remove_imports: bool,
pub(crate) pseudo_classes: Option<PseudoClasses<'a>>,
context: Option<&'a StyleContext<'a, 'b>>,
}
impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> {
/// Create a new Printer wrapping the given destination.
pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Self {
Printer {
sources: None,
dest,
#[cfg(feature = "sourcemap")]
source_map: options.source_map,
#[cfg(feature = "sourcemap")]
source_maps: Vec::new(),
loc: Location {
source_index: 0,
line: 0,
column: 1,
},
indent: 0,
line: 0,
col: 0,
minify: options.minify,
targets: TargetsWithSupportsScope::new(options.targets),
vendor_prefix: VendorPrefix::empty(),
in_calc: false,
css_module: None,
dependencies: if options.analyze_dependencies.is_some() {
Some(Vec::new())
} else {
None
},
remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports),
pseudo_classes: options.pseudo_classes,
context: None,
}
}
/// Returns the current source filename that is being printed.
pub fn filename(&self) -> &'c str {
if let Some(sources) = self.sources {
if let Some(f) = sources.get(self.loc.source_index as usize) {
f
} else {
"unknown.css"
}
} else {
"unknown.css"
}
}
/// Writes a raw string to the underlying destination.
///
/// NOTE: Is is assumed that the string does not contain any newline characters.
/// If such a string is written, it will break source maps.
pub fn write_str(&mut self, s: &str) -> Result<(), PrinterError> {
self.col += s.len() as u32;
self.dest.write_str(s)?;
Ok(())
}
/// Writes a raw string which may contain newlines to the underlying destination.
pub fn write_str_with_newlines(&mut self, s: &str) -> Result<(), PrinterError> {
let mut last_line_start: usize = 0;
for (idx, n) in s.char_indices() {
if n == '\n' {
self.line += 1;
self.col = 0;
// Keep track of where the *next* line starts
last_line_start = idx + 1;
}
}
self.col += (s.len() - last_line_start) as u32;
self.dest.write_str(s)?;
Ok(())
}
/// Write a single character to the underlying destination.
pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> {
if c == '\n' {
self.line += 1;
self.col = 0;
} else {
self.col += 1;
}
self.dest.write_char(c)?;
Ok(())
}
/// Writes a single whitespace character, unless the `minify` option is enabled.
///
/// Use `write_char` instead if you wish to force a space character to be written,
/// regardless of the `minify` option.
pub fn whitespace(&mut self) -> Result<(), PrinterError> {
if self.minify {
return Ok(());
}
self.write_char(' ')
}
/// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
/// If `ws_before` is true, then whitespace is also written before the delimiter.
pub fn delim(&mut self, delim: char, ws_before: bool) -> Result<(), PrinterError> {
if ws_before {
self.whitespace()?;
}
self.write_char(delim)?;
self.whitespace()
}
/// Writes a newline character followed by indentation.
/// If the `minify` option is enabled, then nothing is printed.
pub fn newline(&mut self) -> Result<(), PrinterError> {
if self.minify {
return Ok(());
}
self.write_char('\n')?;
if self.indent > 0 {
self.write_str(&" ".repeat(self.indent as usize))?;
}
Ok(())
}
/// Increases the current indent level.
pub fn indent(&mut self) {
self.indent += 2;
}
/// Decreases the current indent level.
pub fn dedent(&mut self) {
self.indent -= 2;
}
/// Increases the current indent level by the given number of characters.
pub fn indent_by(&mut self, amt: u8) {
self.indent += amt;
}
/// Decreases the current indent level by the given number of characters.
pub fn dedent_by(&mut self, amt: u8) {
self.indent -= amt;
}
/// Returns whether the indent level is greater than one.
pub fn is_nested(&self) -> bool {
self.indent > 2
}
/// Adds a mapping to the source map, if any.
#[cfg(feature = "sourcemap")]
#[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
pub fn add_mapping(&mut self, loc: Location) {
self.loc = loc;
if let Some(map) = &mut self.source_map {
let mut original = OriginalLocation {
original_line: loc.line,
original_column: loc.column - 1,
source: loc.source_index,
name: None,
};
// Remap using input source map if possible.
if let Some(Some(sm)) = self.source_maps.get_mut(loc.source_index as usize) {
let mut found_mapping = false;
if let Some(mapping) = sm.find_closest_mapping(loc.line, loc.column - 1) {
if let Some(orig) = mapping.original {
let sources_len = map.get_sources().len();
let source_index = map.add_source(sm.get_source(orig.source).unwrap());
let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap()));
original.original_line = orig.original_line;
original.original_column = orig.original_column;
original.source = source_index;
original.name = name;
if map.get_sources().len() > sources_len {
let content = sm.get_source_content(orig.source).unwrap().to_owned();
let _ = map.set_source_content(source_index as usize, &content);
}
found_mapping = true;
}
}
if !found_mapping {
return;
}
}
map.add_mapping(self.line, self.col, Some(original))
}
}
/// Writes a CSS identifier to the underlying destination, escaping it
/// as appropriate. If the `css_modules` option was enabled, then a hash
/// is added, and the mapping is added to the CSS module.
pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> {
if handle_css_module {
if let Some(css_module) = &mut self.css_module {
let dest = &mut self.dest;
let mut first = true;
css_module.config.pattern.write(
&css_module.hashes[self.loc.source_index as usize],
&css_module.sources[self.loc.source_index as usize],
ident,
if let Some(content_hashes) = &css_module.content_hashes {
&content_hashes[self.loc.source_index as usize]
} else {
""
},
|s| {
self.col += s.len() as u32;
if first {
first = false;
serialize_identifier(s, dest)
} else {
serialize_name(s, dest)
}
},
)?;
css_module.add_local(&ident, &ident, self.loc.source_index);
return Ok(());
}
}
serialize_identifier(ident, self)?;
Ok(())
}
pub(crate) fn write_dashed_ident(&mut self, ident: &str, is_declaration: bool) -> Result<(), PrinterError> {
self.write_str("--")?;
match &mut self.css_module {
Some(css_module) if css_module.config.dashed_idents => {
let dest = &mut self.dest;
css_module.config.pattern.write(
&css_module.hashes[self.loc.source_index as usize],
&css_module.sources[self.loc.source_index as usize],
&ident[2..],
if let Some(content_hashes) = &css_module.content_hashes {
&content_hashes[self.loc.source_index as usize]
} else {
""
},
|s| {
self.col += s.len() as u32;
serialize_name(s, dest)
},
)?;
if is_declaration {
css_module.add_dashed(ident, self.loc.source_index);
}
}
_ => {
serialize_name(&ident[2..], self)?;
}
}
Ok(())
}
/// Returns an error of the given kind at the provided location in the current source file.
pub fn error(&self, kind: PrinterErrorKind, loc: crate::dependencies::Location) -> Error<PrinterErrorKind> {
Error {
kind,
loc: Some(ErrorLocation {
filename: self.filename().into(),
line: loc.line - 1,
column: loc.column,
}),
}
}
pub(crate) fn with_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
&mut self,
selectors: &SelectorList,
f: F,
) -> Result<T, U> {
let parent = std::mem::take(&mut self.context);
let ctx = StyleContext {
selectors: unsafe { std::mem::transmute(selectors) },
parent,
};
// I can't figure out what lifetime to use here to convince the compiler that
// the reference doesn't live beyond the function call.
self.context = Some(unsafe { std::mem::transmute(&ctx) });
let res = f(self);
self.context = parent;
res
}
pub(crate) fn with_cleared_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
&mut self,
f: F,
) -> Result<T, U> {
let parent = std::mem::take(&mut self.context);
let res = f(self);
self.context = parent;
res
}
pub(crate) fn with_parent_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
&mut self,
f: F,
) -> Result<T, U> {
let parent = std::mem::take(&mut self.context);
if let Some(parent) = parent {
self.context = parent.parent;
}
let res = f(self);
self.context = parent;
res
}
pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> {
self.context.clone()
}
}
impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.col += s.len() as u32;
self.dest.write_str(s)
}
}