grass/crates/compiler/src/serializer.rs
connorskees 942bb20d75 revert float formatting change
this change does fix more number tests, but i'm not certain that
it doesn't break cases we don't test for.

it only solves extremely niche edge cases, so for now reverting
until we can be more certain in a later release
2023-02-01 02:42:19 +00:00

1184 lines
37 KiB
Rust

use std::io::Write;
use codemap::{CodeMap, Span};
use crate::{
ast::{CssStmt, MediaQuery, Style, SupportsRule},
color::{Color, ColorFormat, NAMED_COLORS},
common::{BinaryOp, Brackets, ListSeparator, QuoteKind},
error::SassResult,
selector::{
Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo,
SelectorList, SimpleSelector,
},
utils::hex_char_for,
value::{
fuzzy_equals, ArgList, CalculationArg, CalculationName, SassCalculation, SassFunction,
SassMap, SassNumber, Value,
},
Options,
};
pub(crate) fn serialize_selector_list(
list: &SelectorList,
options: &Options,
span: Span,
) -> String {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, false, span);
serializer.write_selector_list(list);
serializer.finish_for_expr()
}
pub(crate) fn serialize_calculation_arg(
arg: &CalculationArg,
options: &Options,
span: Span,
) -> SassResult<String> {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, false, span);
serializer.write_calculation_arg(arg)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn serialize_number(
number: &SassNumber,
options: &Options,
span: Span,
) -> SassResult<String> {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, false, span);
serializer.visit_number(number)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn serialize_value(val: &Value, options: &Options, span: Span) -> SassResult<String> {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, false, span);
serializer.visit_value(val, span)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn inspect_value(val: &Value, options: &Options, span: Span) -> SassResult<String> {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, true, span);
serializer.visit_value(val, span)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn inspect_float(number: f64, options: &Options, span: Span) -> String {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, true, span);
serializer.write_float(number);
serializer.finish_for_expr()
}
pub(crate) fn inspect_map(map: &SassMap, options: &Options, span: Span) -> SassResult<String> {
let code_map = CodeMap::new();
let mut serializer = Serializer::new(options, &code_map, true, span);
serializer.visit_map(map, span)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn inspect_function_ref(
func: &SassFunction,
options: &Options,
span: Span,
) -> SassResult<String> {
let code_map = CodeMap::new();
let mut serializer = Serializer::new(options, &code_map, true, span);
serializer.visit_function_ref(func, span)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn inspect_number(
number: &SassNumber,
options: &Options,
span: Span,
) -> SassResult<String> {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, true, span);
serializer.visit_number(number)?;
Ok(serializer.finish_for_expr())
}
pub(crate) struct Serializer<'a> {
indentation: usize,
options: &'a Options<'a>,
inspect: bool,
indent_width: usize,
// todo: use this field
_quote: bool,
buffer: Vec<u8>,
map: &'a CodeMap,
span: Span,
}
impl<'a> Serializer<'a> {
pub fn new(options: &'a Options<'a>, map: &'a CodeMap, inspect: bool, span: Span) -> Self {
Self {
inspect,
_quote: true,
indentation: 0,
indent_width: 2,
options,
buffer: Vec::new(),
map,
span,
}
}
fn omit_spaces_around_complex_component(&self, component: &ComplexSelectorComponent) -> bool {
self.options.is_compressed()
&& matches!(component, ComplexSelectorComponent::Combinator(..))
}
fn write_pseudo_selector(&mut self, pseudo: &Pseudo) {
if let Some(sel) = &pseudo.selector {
if pseudo.name == "not" && sel.is_invisible() {
return;
}
}
self.buffer.push(b':');
if !pseudo.is_syntactic_class {
self.buffer.push(b':');
}
self.buffer.extend_from_slice(pseudo.name.as_bytes());
if pseudo.argument.is_none() && pseudo.selector.is_none() {
return;
}
self.buffer.push(b'(');
if let Some(arg) = &pseudo.argument {
self.buffer.extend_from_slice(arg.as_bytes());
if pseudo.selector.is_some() {
self.buffer.push(b' ');
}
}
if let Some(sel) = &pseudo.selector {
self.write_selector_list(sel);
}
self.buffer.push(b')');
}
fn write_namespace(&mut self, namespace: &Namespace) {
match namespace {
Namespace::Empty => self.buffer.push(b'|'),
Namespace::Asterisk => self.buffer.extend_from_slice(b"*|"),
Namespace::Other(namespace) => {
self.buffer.extend_from_slice(namespace.as_bytes());
self.buffer.push(b'|');
}
Namespace::None => {}
}
}
fn write_simple_selector(&mut self, simple: &SimpleSelector) {
match simple {
SimpleSelector::Id(name) => {
self.buffer.push(b'#');
self.buffer.extend_from_slice(name.as_bytes());
}
SimpleSelector::Class(name) => {
self.buffer.push(b'.');
self.buffer.extend_from_slice(name.as_bytes());
}
SimpleSelector::Placeholder(name) => {
self.buffer.push(b'%');
self.buffer.extend_from_slice(name.as_bytes());
}
SimpleSelector::Universal(namespace) => {
self.write_namespace(namespace);
self.buffer.push(b'*');
}
SimpleSelector::Pseudo(pseudo) => self.write_pseudo_selector(pseudo),
SimpleSelector::Type(name) => {
self.write_namespace(&name.namespace);
self.buffer.extend_from_slice(name.ident.as_bytes());
}
SimpleSelector::Attribute(attr) => write!(&mut self.buffer, "{}", attr).unwrap(),
SimpleSelector::Parent(..) => unreachable!("It should not be possible to format `&`."),
}
}
fn write_compound_selector(&mut self, compound: &CompoundSelector) {
let mut did_write = false;
for simple in &compound.components {
if did_write {
self.write_simple_selector(simple);
} else {
let len = self.buffer.len();
self.write_simple_selector(simple);
if self.buffer.len() != len {
did_write = true;
}
}
}
// If we emit an empty compound, it's because all of the components got
// optimized out because they match all selectors, so we just emit the
// universal selector.
if !did_write {
self.buffer.push(b'*');
}
}
fn write_complex_selector_component(&mut self, component: &ComplexSelectorComponent) {
match component {
ComplexSelectorComponent::Combinator(Combinator::NextSibling) => self.buffer.push(b'+'),
ComplexSelectorComponent::Combinator(Combinator::Child) => self.buffer.push(b'>'),
ComplexSelectorComponent::Combinator(Combinator::FollowingSibling) => {
self.buffer.push(b'~')
}
ComplexSelectorComponent::Compound(compound) => self.write_compound_selector(compound),
}
}
fn write_complex_selector(&mut self, complex: &ComplexSelector) {
let mut last_component = None;
for component in &complex.components {
if let Some(c) = last_component {
if !self.omit_spaces_around_complex_component(c)
&& !self.omit_spaces_around_complex_component(component)
{
self.buffer.push(b' ');
}
}
self.write_complex_selector_component(component);
last_component = Some(component);
}
}
fn write_selector_list(&mut self, list: &SelectorList) {
let complexes = list.components.iter().filter(|c| !c.is_invisible());
let mut first = true;
for complex in complexes {
if first {
first = false;
} else {
self.buffer.push(b',');
if complex.line_break {
self.write_newline();
} else {
self.write_optional_space();
}
}
self.write_complex_selector(complex);
}
}
fn write_newline(&mut self) {
if !self.options.is_compressed() {
self.buffer.push(b'\n');
}
}
fn write_comma_separator(&mut self) {
self.buffer.push(b',');
self.write_optional_space();
}
fn write_calculation_name(&mut self, name: CalculationName) {
match name {
CalculationName::Calc => self.buffer.extend_from_slice(b"calc"),
CalculationName::Min => self.buffer.extend_from_slice(b"min"),
CalculationName::Max => self.buffer.extend_from_slice(b"max"),
CalculationName::Clamp => self.buffer.extend_from_slice(b"clamp"),
}
}
fn visit_calculation(&mut self, calculation: &SassCalculation) -> SassResult<()> {
self.write_calculation_name(calculation.name);
self.buffer.push(b'(');
if let Some((last, slice)) = calculation.args.split_last() {
for arg in slice {
self.write_calculation_arg(arg)?;
self.write_comma_separator();
}
self.write_calculation_arg(last)?;
}
self.buffer.push(b')');
Ok(())
}
fn write_calculation_arg(&mut self, arg: &CalculationArg) -> SassResult<()> {
match arg {
CalculationArg::Number(num) => self.visit_number(num)?,
CalculationArg::Calculation(calc) => {
self.visit_calculation(calc)?;
}
CalculationArg::String(s) | CalculationArg::Interpolation(s) => {
self.buffer.extend_from_slice(s.as_bytes());
}
CalculationArg::Operation { lhs, op, rhs } => {
let paren_left = match &**lhs {
CalculationArg::Interpolation(..) => true,
CalculationArg::Operation { op: op2, .. } => op2.precedence() < op.precedence(),
_ => false,
};
if paren_left {
self.buffer.push(b'(');
}
self.write_calculation_arg(lhs)?;
if paren_left {
self.buffer.push(b')');
}
let operator_whitespace =
!self.options.is_compressed() || matches!(op, BinaryOp::Plus | BinaryOp::Minus);
if operator_whitespace {
self.buffer.push(b' ');
}
// todo: avoid allocation with `write_binary_operator` method
self.buffer.extend_from_slice(op.to_string().as_bytes());
if operator_whitespace {
self.buffer.push(b' ');
}
let paren_right = match &**rhs {
CalculationArg::Interpolation(..) => true,
CalculationArg::Operation { op: op2, .. } => {
CalculationArg::parenthesize_calculation_rhs(*op, *op2)
}
_ => false,
};
if paren_right {
self.buffer.push(b'(');
}
self.write_calculation_arg(rhs)?;
if paren_right {
self.buffer.push(b')');
}
}
}
Ok(())
}
fn write_rgb(&mut self, color: &Color) {
let is_opaque = fuzzy_equals(color.alpha().0, 1.0);
if is_opaque {
self.buffer.extend_from_slice(b"rgb(");
} else {
self.buffer.extend_from_slice(b"rgba(");
}
self.write_float(color.red().0);
self.buffer.extend_from_slice(b",");
self.write_optional_space();
self.write_float(color.green().0);
self.buffer.extend_from_slice(b",");
self.write_optional_space();
self.write_float(color.blue().0);
if !is_opaque {
self.buffer.extend_from_slice(b",");
self.write_optional_space();
self.write_float(color.alpha().0);
}
self.buffer.push(b')');
}
fn write_hsl(&mut self, color: &Color) {
let is_opaque = fuzzy_equals(color.alpha().0, 1.0);
if is_opaque {
self.buffer.extend_from_slice(b"hsl(");
} else {
self.buffer.extend_from_slice(b"hsla(");
}
self.write_float(color.hue().0);
self.buffer.extend_from_slice(b"deg, ");
self.write_float(color.saturation().0);
self.buffer.extend_from_slice(b"%, ");
self.write_float(color.lightness().0);
self.buffer.extend_from_slice(b"%");
if !is_opaque {
self.buffer.extend_from_slice(b", ");
self.write_float(color.alpha().0);
}
self.buffer.push(b')');
}
fn write_hex_component(&mut self, channel: u32) {
debug_assert!(channel < 256);
self.buffer.push(hex_char_for(channel >> 4) as u8);
self.buffer.push(hex_char_for(channel & 0xF) as u8);
}
fn is_symmetrical_hex(channel: u32) -> bool {
channel & 0xF == channel >> 4
}
fn can_use_short_hex(color: &Color) -> bool {
Self::is_symmetrical_hex(color.red().0.round() as u32)
&& Self::is_symmetrical_hex(color.green().0.round() as u32)
&& Self::is_symmetrical_hex(color.blue().0.round() as u32)
}
pub fn visit_color(&mut self, color: &Color) {
let red = color.red().0.round() as u8;
let green = color.green().0.round() as u8;
let blue = color.blue().0.round() as u8;
let name = if fuzzy_equals(color.alpha().0, 1.0) {
NAMED_COLORS.get_by_rgba([red, green, blue])
} else {
None
};
#[allow(clippy::unnecessary_unwrap)]
if self.options.is_compressed() {
if fuzzy_equals(color.alpha().0, 1.0) {
let hex_length = if Self::can_use_short_hex(color) { 4 } else { 7 };
if name.is_some() && name.unwrap().len() <= hex_length {
self.buffer.extend_from_slice(name.unwrap().as_bytes());
} else if Self::can_use_short_hex(color) {
self.buffer.push(b'#');
self.buffer.push(hex_char_for(red as u32 & 0xF) as u8);
self.buffer.push(hex_char_for(green as u32 & 0xF) as u8);
self.buffer.push(hex_char_for(blue as u32 & 0xF) as u8);
} else {
self.buffer.push(b'#');
self.write_hex_component(red as u32);
self.write_hex_component(green as u32);
self.write_hex_component(blue as u32);
}
} else {
self.write_rgb(color);
}
} else if color.format != ColorFormat::Infer {
match &color.format {
ColorFormat::Rgb => self.write_rgb(color),
ColorFormat::Hsl => self.write_hsl(color),
ColorFormat::Literal(text) => self.buffer.extend_from_slice(text.as_bytes()),
ColorFormat::Infer => unreachable!(),
}
// Always emit generated transparent colors in rgba format. This works
// around an IE bug. See sass/sass#1782.
} else if name.is_some() && !fuzzy_equals(color.alpha().0, 0.0) {
self.buffer.extend_from_slice(name.unwrap().as_bytes());
} else if fuzzy_equals(color.alpha().0, 1.0) {
self.buffer.push(b'#');
self.write_hex_component(red as u32);
self.write_hex_component(green as u32);
self.write_hex_component(blue as u32);
} else {
self.write_rgb(color);
}
}
fn write_media_query(&mut self, query: &MediaQuery) {
if let Some(modifier) = &query.modifier {
self.buffer.extend_from_slice(modifier.as_bytes());
self.buffer.push(b' ');
}
if let Some(media_type) = &query.media_type {
self.buffer.extend_from_slice(media_type.as_bytes());
if !query.conditions.is_empty() {
self.buffer.extend_from_slice(b" and ");
}
}
if query.conditions.len() == 1 && query.conditions.first().unwrap().starts_with("(not ") {
self.buffer.extend_from_slice(b"not ");
let condition = query.conditions.first().unwrap();
self.buffer
.extend_from_slice(condition["(not ".len()..condition.len() - 1].as_bytes());
} else {
let operator = if query.conjunction { " and " } else { " or " };
self.buffer
.extend_from_slice(query.conditions.join(operator).as_bytes());
}
}
pub fn visit_number(&mut self, number: &SassNumber) -> SassResult<()> {
if let Some(as_slash) = &number.as_slash {
self.visit_number(&as_slash.0)?;
self.buffer.push(b'/');
self.visit_number(&as_slash.1)?;
return Ok(());
}
if !self.inspect && number.unit.is_complex() {
return Err((
format!(
"{} isn't a valid CSS value.",
inspect_number(number, self.options, self.span)?
),
self.span,
)
.into());
}
self.write_float(number.num.0);
write!(&mut self.buffer, "{}", number.unit)?;
Ok(())
}
fn write_float(&mut self, float: f64) {
if float.is_infinite() && float.is_sign_negative() {
self.buffer.extend_from_slice(b"-Infinity");
return;
} else if float.is_infinite() {
self.buffer.extend_from_slice(b"Infinity");
return;
}
// todo: can optimize away intermediate buffer
let mut buffer = String::with_capacity(3);
if float < 0.0 {
buffer.push('-');
}
let num = float.abs();
if self.options.is_compressed() && num < 1.0 {
buffer.push_str(
format!("{:.10}", num)[1..]
.trim_end_matches('0')
.trim_end_matches('.'),
);
} else {
buffer.push_str(
format!("{:.10}", num)
.trim_end_matches('0')
.trim_end_matches('.'),
);
}
if buffer.is_empty() || buffer == "-" || buffer == "-0" {
buffer = "0".to_owned();
}
self.buffer.append(&mut buffer.into_bytes());
}
pub fn visit_group(
&mut self,
stmt: CssStmt,
prev_was_group_end: bool,
prev_requires_semicolon: bool,
) -> SassResult<()> {
if prev_requires_semicolon {
self.buffer.push(b';');
}
if !self.buffer.is_empty() {
self.write_optional_newline();
}
if prev_was_group_end && !self.buffer.is_empty() {
self.write_optional_newline();
}
self.visit_stmt(stmt)?;
Ok(())
}
fn finish_for_expr(self) -> String {
// SAFETY: todo
unsafe { String::from_utf8_unchecked(self.buffer) }
}
pub fn finish(mut self, prev_requires_semicolon: bool) -> String {
let is_not_ascii = self.buffer.iter().any(|&c| !c.is_ascii());
if prev_requires_semicolon {
self.buffer.push(b';');
}
if !self.buffer.is_empty() {
self.write_optional_newline();
}
// SAFETY: todo
let mut as_string = unsafe { String::from_utf8_unchecked(self.buffer) };
if is_not_ascii && self.options.is_compressed() {
as_string.insert(0, '\u{FEFF}');
} else if is_not_ascii {
as_string.insert_str(0, "@charset \"UTF-8\";\n");
}
as_string
}
fn write_indentation(&mut self) {
if self.options.is_compressed() {
return;
}
self.buffer.reserve(self.indentation);
for _ in 0..self.indentation {
self.buffer.push(b' ');
}
}
fn write_list_separator(&mut self, sep: ListSeparator) {
match (sep, self.options.is_compressed()) {
(ListSeparator::Space | ListSeparator::Undecided, _) => self.buffer.push(b' '),
(ListSeparator::Comma, true) => self.buffer.push(b','),
(ListSeparator::Comma, false) => self.buffer.extend_from_slice(b", "),
(ListSeparator::Slash, true) => self.buffer.push(b'/'),
(ListSeparator::Slash, false) => self.buffer.extend_from_slice(b" / "),
}
}
fn elem_needs_parens(sep: ListSeparator, elem: &Value) -> bool {
match elem {
Value::List(elems, sep2, brackets) => {
if elems.len() < 2 {
return false;
}
if *brackets == Brackets::Bracketed {
return false;
}
match sep {
ListSeparator::Comma => *sep2 == ListSeparator::Comma,
ListSeparator::Slash => {
*sep2 == ListSeparator::Comma || *sep2 == ListSeparator::Slash
}
_ => *sep2 != ListSeparator::Undecided,
}
}
_ => false,
}
}
fn visit_list(
&mut self,
list_elems: &[Value],
sep: ListSeparator,
brackets: Brackets,
span: Span,
) -> SassResult<()> {
if brackets == Brackets::Bracketed {
self.buffer.push(b'[');
} else if list_elems.is_empty() {
if !self.inspect {
return Err(("() isn't a valid CSS value.", span).into());
}
self.buffer.extend_from_slice(b"()");
return Ok(());
}
let is_singleton = self.inspect
&& list_elems.len() == 1
&& (sep == ListSeparator::Comma || sep == ListSeparator::Slash);
if is_singleton && brackets != Brackets::Bracketed {
self.buffer.push(b'(');
}
let (mut x, mut y);
let elems: &mut dyn Iterator<Item = &Value> = if self.inspect {
x = list_elems.iter();
&mut x
} else {
y = list_elems.iter().filter(|elem| !elem.is_blank());
&mut y
};
let mut elems = elems.peekable();
while let Some(elem) = elems.next() {
if self.inspect {
let needs_parens = Self::elem_needs_parens(sep, elem);
if needs_parens {
self.buffer.push(b'(');
}
self.visit_value(elem, span)?;
if needs_parens {
self.buffer.push(b')');
}
} else {
self.visit_value(elem, span)?;
}
if elems.peek().is_some() {
self.write_list_separator(sep);
}
}
if is_singleton {
match sep {
ListSeparator::Comma => self.buffer.push(b','),
ListSeparator::Slash => self.buffer.push(b'/'),
_ => unreachable!(),
}
if brackets != Brackets::Bracketed {
self.buffer.push(b')');
}
}
if brackets == Brackets::Bracketed {
self.buffer.push(b']');
}
Ok(())
}
fn write_map_element(&mut self, value: &Value, span: Span) -> SassResult<()> {
let needs_parens = matches!(value, Value::List(_, ListSeparator::Comma, Brackets::None));
if needs_parens {
self.buffer.push(b'(');
}
self.visit_value(value, span)?;
if needs_parens {
self.buffer.push(b')');
}
Ok(())
}
fn visit_map(&mut self, map: &SassMap, span: Span) -> SassResult<()> {
if !self.inspect {
return Err((
format!(
"{} isn't a valid CSS value.",
inspect_map(map, self.options, span)?
),
span,
)
.into());
}
self.buffer.push(b'(');
let mut elems = map.iter().peekable();
while let Some((k, v)) = elems.next() {
self.write_map_element(&k.node, k.span)?;
self.buffer.extend_from_slice(b": ");
self.write_map_element(v, k.span)?;
if elems.peek().is_some() {
self.buffer.extend_from_slice(b", ");
}
}
self.buffer.push(b')');
Ok(())
}
fn visit_unquoted_string(&mut self, string: &str) {
let mut after_newline = false;
self.buffer.reserve(string.len());
for c in string.bytes() {
match c {
b'\n' => {
self.buffer.push(b' ');
after_newline = true;
}
b' ' => {
if !after_newline {
self.buffer.push(b' ');
}
}
_ => {
self.buffer.push(c);
after_newline = false;
}
}
}
}
fn visit_quoted_string(&mut self, force_double_quote: bool, string: &str) {
let mut has_single_quote = false;
let mut has_double_quote = false;
let mut buffer = Vec::new();
if force_double_quote {
buffer.push(b'"');
}
let mut iter = string.as_bytes().iter().copied().peekable();
while let Some(c) = iter.next() {
match c {
b'\'' => {
if force_double_quote {
buffer.push(b'\'');
} else if has_double_quote {
self.visit_quoted_string(true, string);
return;
} else {
has_single_quote = true;
buffer.push(b'\'');
}
}
b'"' => {
if force_double_quote {
buffer.push(b'\\');
buffer.push(b'"');
} else if has_single_quote {
self.visit_quoted_string(true, string);
return;
} else {
has_double_quote = true;
buffer.push(b'"');
}
}
b'\x00'..=b'\x08' | b'\x0A'..=b'\x1F' => {
buffer.push(b'\\');
if c as u32 > 0xF {
buffer.push(hex_char_for(c as u32 >> 4) as u8);
}
buffer.push(hex_char_for(c as u32 & 0xF) as u8);
let next = match iter.peek() {
Some(v) => *v,
None => break,
};
if next.is_ascii_hexdigit() || next == b' ' || next == b'\t' {
buffer.push(b' ');
}
}
b'\\' => {
buffer.push(b'\\');
buffer.push(b'\\');
}
_ => buffer.push(c),
}
}
if force_double_quote {
buffer.push(b'"');
self.buffer.extend_from_slice(&buffer);
} else {
let quote = if has_double_quote { b'\'' } else { b'"' };
self.buffer.push(quote);
self.buffer.extend_from_slice(&buffer);
self.buffer.push(quote);
}
}
fn visit_function_ref(&mut self, func: &SassFunction, span: Span) -> SassResult<()> {
if !self.inspect {
return Err((
format!(
"{} isn't a valid CSS value.",
inspect_function_ref(func, self.options, span)?
),
span,
)
.into());
}
self.buffer.extend_from_slice(b"get-function(");
self.visit_quoted_string(false, func.name().as_str());
self.buffer.push(b')');
Ok(())
}
fn visit_arglist(&mut self, arglist: &ArgList, span: Span) -> SassResult<()> {
self.visit_list(&arglist.elems, ListSeparator::Comma, Brackets::None, span)
}
fn visit_value(&mut self, value: &Value, span: Span) -> SassResult<()> {
match value {
Value::Dimension(num) => self.visit_number(num)?,
Value::Color(color) => self.visit_color(color),
Value::Calculation(calc) => self.visit_calculation(calc)?,
Value::List(elems, sep, brackets) => self.visit_list(elems, *sep, *brackets, span)?,
Value::True => self.buffer.extend_from_slice(b"true"),
Value::False => self.buffer.extend_from_slice(b"false"),
Value::Null => {
if self.inspect {
self.buffer.extend_from_slice(b"null")
}
}
Value::Map(map) => self.visit_map(map, span)?,
Value::FunctionRef(func) => self.visit_function_ref(func, span)?,
Value::String(s, QuoteKind::Quoted) => self.visit_quoted_string(false, s),
Value::String(s, QuoteKind::None) => self.visit_unquoted_string(s),
Value::ArgList(arglist) => self.visit_arglist(arglist, span)?,
}
Ok(())
}
fn write_style(&mut self, style: Style) -> SassResult<()> {
if !self.options.is_compressed() {
self.write_indentation();
}
self.buffer
.extend_from_slice(style.property.resolve_ref().as_bytes());
self.buffer.push(b':');
// todo: _writeFoldedValue and _writeReindentedValue
if !style.declared_as_custom_property && !self.options.is_compressed() {
self.buffer.push(b' ');
}
self.visit_value(&style.value.node, style.value.span)?;
Ok(())
}
fn write_import(&mut self, import: &str, modifiers: Option<String>) -> SassResult<()> {
self.write_indentation();
self.buffer.extend_from_slice(b"@import ");
write!(&mut self.buffer, "{}", import)?;
if let Some(modifiers) = modifiers {
self.buffer.push(b' ');
self.buffer.extend_from_slice(modifiers.as_bytes());
}
Ok(())
}
fn write_comment(&mut self, comment: &str, span: Span) -> SassResult<()> {
if self.options.is_compressed() && !comment.starts_with("/*!") {
return Ok(());
}
self.write_indentation();
let col = self.map.look_up_pos(span.low()).position.column;
let mut lines = comment.lines();
if let Some(line) = lines.next() {
self.buffer.extend_from_slice(line.trim_start().as_bytes());
}
let lines = lines
.map(|line| {
let diff = (line.len() - line.trim_start().len()).saturating_sub(col);
format!("{}{}", " ".repeat(diff), line.trim_start())
})
.collect::<Vec<String>>()
.join("\n");
if !lines.is_empty() {
write!(&mut self.buffer, "\n{}", lines)?;
}
Ok(())
}
pub fn requires_semicolon(stmt: &CssStmt) -> bool {
match stmt {
CssStmt::Style(_) | CssStmt::Import(_, _) => true,
CssStmt::UnknownAtRule(rule, _) => !rule.has_body,
_ => false,
}
}
fn write_children(&mut self, mut children: Vec<CssStmt>) -> SassResult<()> {
if self.options.is_compressed() {
self.buffer.push(b'{');
} else {
self.buffer.extend_from_slice(b" {\n");
}
self.indentation += self.indent_width;
let last = children.pop();
for child in children {
let needs_semicolon = Self::requires_semicolon(&child);
let did_write = self.visit_stmt(child)?;
if !did_write {
continue;
}
if needs_semicolon {
self.buffer.push(b';');
}
self.write_optional_newline();
}
if let Some(last) = last {
let needs_semicolon = Self::requires_semicolon(&last);
let did_write = self.visit_stmt(last)?;
if did_write {
if needs_semicolon && !self.options.is_compressed() {
self.buffer.push(b';');
}
self.write_optional_newline();
}
}
self.indentation -= self.indent_width;
if self.options.is_compressed() {
self.buffer.push(b'}');
} else {
self.write_indentation();
self.buffer.extend_from_slice(b"}");
}
Ok(())
}
fn write_optional_space(&mut self) {
if !self.options.is_compressed() {
self.buffer.push(b' ');
}
}
fn write_optional_newline(&mut self) {
if !self.options.is_compressed() {
self.buffer.push(b'\n');
}
}
fn write_supports_rule(&mut self, supports_rule: SupportsRule) -> SassResult<()> {
self.write_indentation();
self.buffer.extend_from_slice(b"@supports");
if !supports_rule.params.is_empty() {
self.buffer.push(b' ');
self.buffer
.extend_from_slice(supports_rule.params.as_bytes());
}
self.write_children(supports_rule.body)?;
Ok(())
}
/// Returns whether or not text was written
fn visit_stmt(&mut self, stmt: CssStmt) -> SassResult<bool> {
if stmt.is_invisible() {
return Ok(false);
}
match stmt {
CssStmt::RuleSet { selector, body, .. } => {
self.write_indentation();
self.write_selector_list(&selector.as_selector_list());
self.write_children(body)?;
}
CssStmt::Media(media_rule, ..) => {
self.write_indentation();
self.buffer.extend_from_slice(b"@media ");
if let Some((last, rest)) = media_rule.query.split_last() {
for query in rest {
self.write_media_query(query);
self.buffer.push(b',');
self.write_optional_space();
}
self.write_media_query(last);
}
self.write_children(media_rule.body)?;
}
CssStmt::UnknownAtRule(unknown_at_rule, ..) => {
self.write_indentation();
self.buffer.push(b'@');
self.buffer
.extend_from_slice(unknown_at_rule.name.as_bytes());
if !unknown_at_rule.params.is_empty() {
write!(&mut self.buffer, " {}", unknown_at_rule.params)?;
}
if !unknown_at_rule.has_body {
debug_assert!(unknown_at_rule.body.is_empty());
return Ok(true);
} else if unknown_at_rule.body.iter().all(CssStmt::is_invisible) {
self.buffer.extend_from_slice(b" {}");
return Ok(true);
}
self.write_children(unknown_at_rule.body)?;
}
CssStmt::Style(style) => self.write_style(style)?,
CssStmt::Comment(comment, span) => self.write_comment(&comment, span)?,
CssStmt::KeyframesRuleSet(keyframes_rule_set) => {
self.write_indentation();
// todo: i bet we can do something like write_with_separator to avoid extra allocation
let selector = keyframes_rule_set
.selector
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ");
self.buffer.extend_from_slice(selector.as_bytes());
self.write_children(keyframes_rule_set.body)?;
}
CssStmt::Import(import, modifier) => self.write_import(&import, modifier)?,
CssStmt::Supports(supports_rule, _) => self.write_supports_rule(supports_rule)?,
}
Ok(true)
}
}