support compressed lists and number values

This commit is contained in:
Connor Skees 2021-07-25 09:53:18 -04:00
parent 3ced8feed5
commit 94fe52a81d
28 changed files with 521 additions and 192 deletions

View File

@ -34,6 +34,7 @@ complex uses of @at-root
media queries with @import media queries with @import
/ as a separator in color functions, e.g. rgba(255, 255, 255 / 0) / as a separator in color functions, e.g. rgba(255, 255, 255 / 0)
Infinity and -Infinity Infinity and -Infinity
builtin meta function keywords
``` ```
All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19). All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19).

View File

@ -64,7 +64,7 @@ impl CallArgs {
CallArgs(HashMap::new(), span) CallArgs(HashMap::new(), span)
} }
pub fn to_css_string(self) -> SassResult<Spanned<String>> { pub fn to_css_string(self, is_compressed: bool) -> SassResult<Spanned<String>> {
let mut string = String::with_capacity(2 + self.len() * 10); let mut string = String::with_capacity(2 + self.len() * 10);
string.push('('); string.push('(');
let mut span = self.1; let mut span = self.1;
@ -88,7 +88,7 @@ impl CallArgs {
.iter() .iter()
.map(|a| { .map(|a| {
span = span.merge(a.span); span = span.merge(a.span);
a.node.to_css_string(a.span) a.node.to_css_string(a.span, is_compressed)
}) })
.collect::<SassResult<Vec<Cow<'static, str>>>>()? .collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(", "), .join(", "),

View File

@ -90,13 +90,17 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let mut string = format!( let mut string = format!(
"{}({}, {}, {}", "{}({}, {}, {}",
name, name,
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
saturation.to_css_string(args.span())?, saturation.to_css_string(args.span(), parser.options.is_compressed())?,
lightness.to_css_string(args.span())? lightness.to_css_string(args.span(), parser.options.is_compressed())?
); );
if !args.is_empty() { if !args.is_empty() {
string.push_str(", "); string.push_str(", ");
string.push_str(&args.get_err(3, "alpha")?.to_css_string(args.span())?); string.push_str(
&args
.get_err(3, "alpha")?
.to_css_string(args.span(), parser.options.is_compressed())?,
);
} }
string.push(')'); string.push(')');
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
@ -117,13 +121,17 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let mut string = format!( let mut string = format!(
"{}({}, {}, {}", "{}({}, {}, {}",
name, name,
hue, hue.to_string(parser.options.is_compressed()),
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
lightness.to_css_string(args.span())? lightness.to_css_string(args.span(), parser.options.is_compressed())?
); );
if !args.is_empty() { if !args.is_empty() {
string.push_str(", "); string.push_str(", ");
string.push_str(&args.get_err(3, "alpha")?.to_css_string(args.span())?); string.push_str(
&args
.get_err(3, "alpha")?
.to_css_string(args.span(), parser.options.is_compressed())?,
);
} }
string.push(')'); string.push(')');
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
@ -132,7 +140,7 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$saturation: {} is not a number.", "$saturation: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -146,13 +154,17 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let mut string = format!( let mut string = format!(
"{}({}, {}, {}", "{}({}, {}, {}",
name, name,
hue, hue.to_string(parser.options.is_compressed()),
saturation, saturation.to_string(parser.options.is_compressed()),
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
); );
if !args.is_empty() { if !args.is_empty() {
string.push_str(", "); string.push_str(", ");
string.push_str(&args.get_err(3, "alpha")?.to_css_string(args.span())?); string.push_str(
&args
.get_err(3, "alpha")?
.to_css_string(args.span(), parser.options.is_compressed())?,
);
} }
string.push(')'); string.push(')');
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
@ -161,7 +173,7 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$lightness: {} is not a number.", "$lightness: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -180,7 +192,7 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$alpha: Expected {} to have no units or \"%\".", "$alpha: Expected {} to have no units or \"%\".",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -191,10 +203,10 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
format!( format!(
"{}({}, {}, {}, {})", "{}({}, {}, {}, {})",
name, name,
hue, hue.to_string(parser.options.is_compressed()),
saturation, saturation.to_string(parser.options.is_compressed()),
lightness, lightness.to_string(parser.options.is_compressed()),
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
@ -276,7 +288,7 @@ pub(crate) fn adjust_hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<
return Err(( return Err((
format!( format!(
"$degrees: {} is not a number.", "$degrees: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -305,7 +317,7 @@ fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
return Err(( return Err((
format!( format!(
"$amount: {} is not a number.", "$amount: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), false)?
), ),
args.span(), args.span(),
) )
@ -334,7 +346,7 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
return Err(( return Err((
format!( format!(
"$amount: {} is not a number.", "$amount: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), false)?
), ),
args.span(), args.span(),
) )
@ -350,7 +362,8 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
return Ok(Value::String( return Ok(Value::String(
format!( format!(
"saturate({})", "saturate({})",
args.get_err(0, "amount")?.to_css_string(args.span())? args.get_err(0, "amount")?
.to_css_string(args.span(), false)?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
@ -363,7 +376,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
return Err(( return Err((
format!( format!(
"$amount: {} is not a number.", "$amount: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), false)?
), ),
args.span(), args.span(),
) )
@ -374,7 +387,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
Value::Color(c) => c, Value::Color(c) => c,
Value::Dimension(Some(n), u, _) => { Value::Dimension(Some(n), u, _) => {
return Ok(Value::String( return Ok(Value::String(
format!("saturate({}{})", n, u), format!("saturate({}{})", n.inspect(), u),
QuoteKind::None, QuoteKind::None,
)) ))
} }
@ -408,7 +421,7 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
return Err(( return Err((
format!( format!(
"$amount: {} is not a number.", "$amount: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -424,7 +437,7 @@ pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
Value::Color(c) => c, Value::Color(c) => c,
Value::Dimension(Some(n), u, _) => { Value::Dimension(Some(n), u, _) => {
return Ok(Value::String( return Ok(Value::String(
format!("grayscale({}{})", n, u), format!("grayscale({}{})", n.inspect(), u),
QuoteKind::None, QuoteKind::None,
)) ))
} }
@ -471,7 +484,7 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
return Err(( return Err((
format!( format!(
"$weight: {} is not a number.", "$weight: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -491,7 +504,7 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
.into()); .into());
} }
Ok(Value::String( Ok(Value::String(
format!("invert({}{})", n, u), format!("invert({}{})", n.inspect(), u),
QuoteKind::None, QuoteKind::None,
)) ))
} }

View File

@ -74,7 +74,7 @@ pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)),
Value::Dimension(Some(num), unit, _) => Ok(Value::String( Value::Dimension(Some(num), unit, _) => Ok(Value::String(
format!("opacity({}{})", num, unit), format!("opacity({}{})", num.inspect(), unit),
QuoteKind::None, QuoteKind::None,
)), )),
Value::Dimension(None, ..) => todo!(), Value::Dimension(None, ..) => todo!(),

View File

@ -50,9 +50,9 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
format!( format!(
"{}({}, {}, {})", "{}({}, {}, {})",
name, name,
red.to_css_string(args.span())?, red.to_css_string(args.span(), parser.options.is_compressed())?,
green.to_css_string(args.span())?, green.to_css_string(args.span(), parser.options.is_compressed())?,
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
@ -78,11 +78,16 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
Some(red) => format!( Some(red) => format!(
"{}({}, {}, {})", "{}({}, {}, {})",
name, name,
red.to_css_string(args.span())?, red.to_css_string(args.span(), parser.options.is_compressed())?,
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
blue blue.to_string(parser.options.is_compressed())
),
None => format!(
"{}({} {})",
name,
v.to_css_string(args.span(), parser.options.is_compressed())?,
blue.to_string(parser.options.is_compressed())
), ),
None => format!("{}({} {})", name, v.to_css_string(args.span())?, blue),
}; };
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
} }
@ -107,9 +112,9 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
format!( format!(
"{}({}, {}, {})", "{}({}, {}, {})",
name, name,
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
green, green.to_string(parser.options.is_compressed()),
blue blue.to_string(parser.options.is_compressed())
), ),
QuoteKind::None, QuoteKind::None,
)); ));
@ -136,8 +141,8 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
format!( format!(
"{}({}, {})", "{}({}, {})",
name, name,
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
alpha.to_css_string(args.span())? alpha.to_css_string(args.span(), parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
@ -158,7 +163,7 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$alpha: Expected {} to have no units or \"%\".", "$alpha: Expected {} to have no units or \"%\".",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -169,10 +174,10 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
format!( format!(
"{}({}, {}, {}, {})", "{}({}, {}, {}, {})",
name, name,
color.red(), color.red().to_string(parser.options.is_compressed()),
color.green(), color.green().to_string(parser.options.is_compressed()),
color.blue(), color.blue().to_string(parser.options.is_compressed()),
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
@ -197,7 +202,7 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$red: Expected {} to have no units or \"%\".", "$red: Expected {} to have no units or \"%\".",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -209,13 +214,17 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let mut string = format!( let mut string = format!(
"{}({}, {}, {}", "{}({}, {}, {}",
name, name,
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
green.to_css_string(args.span())?, green.to_css_string(args.span(), parser.options.is_compressed())?,
blue.to_css_string(args.span())? blue.to_css_string(args.span(), parser.options.is_compressed())?
); );
if !args.is_empty() { if !args.is_empty() {
string.push_str(", "); string.push_str(", ");
string.push_str(&args.get_err(3, "alpha")?.to_css_string(args.span())?); string.push_str(
&args
.get_err(3, "alpha")?
.to_css_string(args.span(), parser.options.is_compressed())?,
);
} }
string.push(')'); string.push(')');
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
@ -238,7 +247,7 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$green: Expected {} to have no units or \"%\".", "$green: Expected {} to have no units or \"%\".",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -249,13 +258,17 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let mut string = format!( let mut string = format!(
"{}({}, {}, {}", "{}({}, {}, {}",
name, name,
red, red.to_string(parser.options.is_compressed()),
v.to_css_string(args.span())?, v.to_css_string(args.span(), parser.options.is_compressed())?,
blue.to_css_string(args.span())? blue.to_css_string(args.span(), parser.options.is_compressed())?
); );
if !args.is_empty() { if !args.is_empty() {
string.push_str(", "); string.push_str(", ");
string.push_str(&args.get_err(3, "alpha")?.to_css_string(args.span())?); string.push_str(
&args
.get_err(3, "alpha")?
.to_css_string(args.span(), parser.options.is_compressed())?,
);
} }
string.push(')'); string.push(')');
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
@ -278,7 +291,7 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$blue: Expected {} to have no units or \"%\".", "$blue: Expected {} to have no units or \"%\".",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -288,13 +301,17 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let mut string = format!( let mut string = format!(
"{}({}, {}, {}", "{}({}, {}, {}",
name, name,
red, red.to_string(parser.options.is_compressed()),
green, green.to_string(parser.options.is_compressed()),
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
); );
if !args.is_empty() { if !args.is_empty() {
string.push_str(", "); string.push_str(", ");
string.push_str(&args.get_err(3, "alpha")?.to_css_string(args.span())?); string.push_str(
&args
.get_err(3, "alpha")?
.to_css_string(args.span(), parser.options.is_compressed())?,
);
} }
string.push(')'); string.push(')');
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
@ -319,7 +336,7 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
return Err(( return Err((
format!( format!(
"$alpha: Expected {} to have no units or \"%\".", "$alpha: Expected {} to have no units or \"%\".",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -329,10 +346,10 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
let string = format!( let string = format!(
"{}({}, {}, {}, {})", "{}({}, {}, {}, {})",
name, name,
red, red.to_string(parser.options.is_compressed()),
green, green.to_string(parser.options.is_compressed()),
blue, blue.to_string(parser.options.is_compressed()),
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
); );
return Ok(Value::String(string, QuoteKind::None)); return Ok(Value::String(string, QuoteKind::None));
} }
@ -429,7 +446,7 @@ pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
return Err(( return Err((
format!( format!(
"$weight: {} is not a number.", "$weight: {} is not a number.",
v.to_css_string(args.span())? v.to_css_string(args.span(), parser.options.is_compressed())?
), ),
args.span(), args.span(),
) )

View File

@ -45,7 +45,7 @@ pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
return Err(( return Err((
format!( format!(
"$n: Invalid index {}{} for a list with {} elements.", "$n: Invalid index {}{} for a list with {} elements.",
n, n.inspect(),
unit, unit,
list.len() list.len()
), ),
@ -55,7 +55,7 @@ pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
if n.is_decimal() { if n.is_decimal() {
return Err((format!("$n: {} is not an int.", n), args.span()).into()); return Err((format!("$n: {} is not an int.", n.inspect()), args.span()).into());
} }
Ok(list.remove(if n.is_positive() { Ok(list.remove(if n.is_positive() {
@ -114,7 +114,9 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
return Err(( return Err((
format!( format!(
"$n: Invalid index {}{} for a list with {} elements.", "$n: Invalid index {}{} for a list with {} elements.",
n, unit, len n.inspect(),
unit,
len
), ),
args.span(), args.span(),
) )
@ -122,7 +124,7 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
} }
if n.is_decimal() { if n.is_decimal() {
return Err((format!("$n: {} is not an int.", n), args.span()).into()); return Err((format!("$n: {} is not an int.", n.inspect()), args.span()).into());
} }
let val = args.get_err(2, "value")?; let val = args.get_err(2, "value")?;

View File

@ -4,7 +4,13 @@ macro_rules! bound {
return Err(( return Err((
format!( format!(
"${}: Expected {}{} to be within {}{} and {}{}.", "${}: Expected {}{} to be within {}{} and {}{}.",
$name, $arg, $unit, $low, $unit, $high, $unit, $name,
$arg.inspect(),
$unit,
$low,
$unit,
$high,
$unit,
), ),
$args.span(), $args.span(),
) )
@ -18,7 +24,13 @@ macro_rules! bound {
return Err(( return Err((
format!( format!(
"${}: Expected {}{} to be within {}{} and {}{}.", "${}: Expected {}{} to be within {}{} and {}{}.",
$name, $arg, $unit, $low, $unit, $high, $unit, $name,
$arg.inspect(),
$unit,
$low,
$unit,
$high,
$unit,
), ),
$args.span(), $args.span(),
) )

View File

@ -149,12 +149,16 @@ pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
} }
if limit.is_decimal() { if limit.is_decimal() {
return Err((format!("$limit: {} is not an int.", limit), args.span()).into()); return Err((
format!("$limit: {} is not an int.", limit.inspect()),
args.span(),
)
.into());
} }
if limit.is_zero() || limit.is_negative() { if limit.is_zero() || limit.is_negative() {
return Err(( return Err((
format!("$limit: Must be greater than 0, was {}.", limit), format!("$limit: Must be greater than 0, was {}.", limit.inspect()),
args.span(), args.span(),
) )
.into()); .into());
@ -164,7 +168,10 @@ pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
Some(n) => n, Some(n) => n,
None => { None => {
return Err(( return Err((
format!("max must be in range 0 < max \u{2264} 2^32, was {}", limit), format!(
"max must be in range 0 < max \u{2264} 2^32, was {}",
limit.inspect()
),
args.span(), args.span(),
) )
.into()) .into())

View File

@ -100,7 +100,7 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
let str_len = string.chars().count(); let str_len = string.chars().count();
let start = match args.get_err(1, "start-at")? { let start = match args.get_err(1, "start-at")? {
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
return Err((format!("{} is not an int.", n), args.span()).into()) return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
} }
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
n.to_integer().to_usize().unwrap_or(str_len + 1) n.to_integer().to_usize().unwrap_or(str_len + 1)
@ -133,7 +133,7 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
}; };
let mut end = match args.default_arg(2, "end-at", Value::Null)? { let mut end = match args.default_arg(2, "end-at", Value::Null)? {
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
return Err((format!("{} is not an int.", n), args.span()).into()) return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
} }
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
n.to_integer().to_usize().unwrap_or(str_len + 1) n.to_integer().to_usize().unwrap_or(str_len + 1)
@ -240,7 +240,11 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
let index = match args.get_err(2, "index")? { let index = match args.get_err(2, "index")? {
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
return Err((format!("$index: {} is not an int.", n), args.span()).into()) return Err((
format!("$index: {} is not an int.", n.inspect()),
args.span(),
)
.into())
} }
Value::Dimension(Some(n), Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(None, Unit::None, ..) => { Value::Dimension(None, Unit::None, ..) => {

View File

@ -590,7 +590,14 @@ fn repr(red: &Number, green: &Number, blue: &Number, alpha: &Number) -> String {
let blue_u8 = into_u8(blue); let blue_u8 = into_u8(blue);
if alpha < &Number::one() { if alpha < &Number::one() {
format!("rgba({}, {}, {}, {})", red_u8, green_u8, blue_u8, alpha) format!(
"rgba({}, {}, {}, {})",
red_u8,
green_u8,
blue_u8,
// todo: is_compressed
alpha.inspect()
)
} else if let Some(c) = NAMED_COLORS.get_by_rgba([red_u8, green_u8, blue_u8]) { } else if let Some(c) = NAMED_COLORS.get_by_rgba([red_u8, green_u8, blue_u8]) {
(*c).to_owned() (*c).to_owned()
} else { } else {

View File

@ -99,6 +99,13 @@ impl ListSeparator {
} }
} }
pub fn as_compressed_str(self) -> &'static str {
match self {
Self::Space => " ",
Self::Comma => ",",
}
}
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
match self { match self {
Self::Space => "space", Self::Space => "space",

View File

@ -260,6 +260,10 @@ impl<'a> Options<'a> {
self.unicode_error_messages = unicode_error_messages; self.unicode_error_messages = unicode_error_messages;
self self
} }
pub(crate) fn is_compressed(&self) -> bool {
matches!(self.style, OutputStyle::Compressed)
}
} }
fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> { fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {

View File

@ -511,7 +511,7 @@ impl Formatter for CompressedFormatter {
write!(buf, "}}")?; write!(buf, "}}")?;
} }
Toplevel::Style(style) => { Toplevel::Style(style) => {
let value = style.value.node.to_css_string(style.value.span)?; let value = style.value.node.to_css_string(style.value.span, true)?;
write!(buf, "{}:{};", style.property, value)?; write!(buf, "{}:{};", style.property, value)?;
} }
} }
@ -542,7 +542,7 @@ impl CompressedFormatter {
for style in &mut styles { for style in &mut styles {
match style { match style {
BlockEntry::Style(s) => { BlockEntry::Style(s) => {
let value = s.value.node.to_css_string(s.value.span)?; let value = s.value.node.to_css_string(s.value.span, true)?;
write!(buf, "{}:{}", s.property, value)?; write!(buf, "{}:{}", s.property, value)?;
break; break;
} }
@ -554,7 +554,7 @@ impl CompressedFormatter {
for style in styles { for style in styles {
match style { match style {
BlockEntry::Style(s) => { BlockEntry::Style(s) => {
let value = s.value.node.to_css_string(s.value.span)?; let value = s.value.node.to_css_string(s.value.span, true)?;
write!(buf, ";{}:{}", s.property, value)?; write!(buf, ";{}:{}", s.property, value)?;
} }

View File

@ -299,8 +299,11 @@ impl<'a, 'b> Parser<'a, 'b> {
let value = format!( let value = format!(
"{}={}", "{}={}",
left.node.to_css_string(left.span)?, left.node
right.node.to_css_string(right.span)? .to_css_string(left.span, self.options.is_compressed())?,
right
.node
.to_css_string(right.span, self.options.is_compressed())?
); );
args.insert( args.insert(

View File

@ -211,7 +211,7 @@ impl<'a, 'b> Parser<'a, 'b> {
let from = match from_val.node { let from = match from_val.node {
Value::Dimension(Some(n), ..) => match n.to_i32() { Value::Dimension(Some(n), ..) => match n.to_i32() {
Some(std::i32::MAX) | Some(std::i32::MIN) | None => { Some(std::i32::MAX) | Some(std::i32::MIN) | None => {
return Err((format!("{} is not an int.", n), from_val.span).into()) return Err((format!("{} is not an int.", n.inspect()), from_val.span).into())
} }
Some(v) => v, Some(v) => v,
}, },
@ -229,14 +229,17 @@ impl<'a, 'b> Parser<'a, 'b> {
let to = match to_val.node { let to = match to_val.node {
Value::Dimension(Some(n), ..) => match n.to_i32() { Value::Dimension(Some(n), ..) => match n.to_i32() {
Some(std::i32::MAX) | Some(std::i32::MIN) | None => { Some(std::i32::MAX) | Some(std::i32::MIN) | None => {
return Err((format!("{} is not an int.", n), to_val.span).into()) return Err((format!("{} is not an int.", n.inspect()), to_val.span).into())
} }
Some(v) => v, Some(v) => v,
}, },
Value::Dimension(None, ..) => return Err(("NaN is not an int.", from_val.span).into()), Value::Dimension(None, ..) => return Err(("NaN is not an int.", from_val.span).into()),
v => { v => {
return Err(( return Err((
format!("{} is not a number.", v.to_css_string(to_val.span)?), format!(
"{} is not a number.",
v.to_css_string(to_val.span, self.options.is_compressed())?
),
to_val.span, to_val.span,
) )
.into()) .into())

View File

@ -64,7 +64,11 @@ impl<'a, 'b> Parser<'a, 'b> {
self.toks.next(); self.toks.next();
// TODO: if ident, interpolate literally // TODO: if ident, interpolate literally
let interpolation = self.parse_interpolation()?; let interpolation = self.parse_interpolation()?;
buf.push_str(&interpolation.node.to_css_string(interpolation.span)?); buf.push_str(
&interpolation
.node
.to_css_string(interpolation.span, self.options.is_compressed())?,
);
} else { } else {
self.toks.reset_cursor(); self.toks.reset_cursor();
break; break;
@ -179,7 +183,10 @@ impl<'a, 'b> Parser<'a, 'b> {
self.toks.next(); self.toks.next();
match self.parse_interpolation()?.node { match self.parse_interpolation()?.node {
Value::String(ref s, ..) => text.push_str(s), Value::String(ref s, ..) => text.push_str(s),
v => text.push_str(v.to_css_string(self.span_before)?.borrow()), v => text.push_str(
v.to_css_string(self.span_before, self.options.is_compressed())?
.borrow(),
),
} }
} }
_ => return Err(("Expected identifier.", pos).into()), _ => return Err(("Expected identifier.", pos).into()),
@ -268,7 +275,10 @@ impl<'a, 'b> Parser<'a, 'b> {
let interpolation = self.parse_interpolation()?; let interpolation = self.parse_interpolation()?;
match interpolation.node { match interpolation.node {
Value::String(ref v, ..) => s.push_str(v), Value::String(ref v, ..) => s.push_str(v),
v => s.push_str(v.to_css_string(interpolation.span)?.borrow()), v => s.push_str(
v.to_css_string(interpolation.span, self.options.is_compressed())?
.borrow(),
),
}; };
continue; continue;
} }

View File

@ -126,7 +126,11 @@ impl<'a, 'b> Parser<'a, 'b> {
match tok.kind { match tok.kind {
'#' => { '#' => {
if self.consume_char_if_exists('{') { if self.consume_char_if_exists('{') {
string.push_str(&self.parse_interpolation()?.to_css_string(span)?); string.push_str(
&self
.parse_interpolation()?
.to_css_string(span, self.options.is_compressed())?,
);
} else { } else {
string.push('#'); string.push('#');
} }

View File

@ -51,7 +51,10 @@ impl<'a, 'b> Parser<'a, 'b> {
_ => false, _ => false,
})?; })?;
value.node.unquote().to_css_string(value.span) value
.node
.unquote()
.to_css_string(value.span, self.options.is_compressed())
} }
pub(super) fn parse_media_query_list(&mut self) -> SassResult<String> { pub(super) fn parse_media_query_list(&mut self) -> SassResult<String> {
@ -92,7 +95,11 @@ impl<'a, 'b> Parser<'a, 'b> {
})?; })?;
self.expect_char(')')?; self.expect_char(')')?;
buf.push_str(&value.node.to_css_string(value.span)?); buf.push_str(
&value
.node
.to_css_string(value.span, self.options.is_compressed())?,
);
self.whitespace_or_comment(); self.whitespace_or_comment();
buf.push(')'); buf.push(')');

View File

@ -210,7 +210,7 @@ impl<'a, 'b> Parser<'a, 'b> {
self.consume_char_if_exists(';'); self.consume_char_if_exists(';');
self.warn(&Spanned { self.warn(&Spanned {
node: message.to_css_string(span)?, node: message.to_css_string(span, false)?,
span, span,
}); });
} }
@ -407,7 +407,11 @@ impl<'a, 'b> Parser<'a, 'b> {
match kind { match kind {
'#' => { '#' => {
if self.consume_char_if_exists('{') { if self.consume_char_if_exists('{') {
string.push_str(&self.parse_interpolation()?.to_css_string(span)?); string.push_str(
&self
.parse_interpolation()?
.to_css_string(span, self.options.is_compressed())?,
);
} else { } else {
string.push('#'); string.push('#');
} }
@ -513,7 +517,11 @@ impl<'a, 'b> Parser<'a, 'b> {
} }
('#', Some(Token { kind: '{', .. })) => { ('#', Some(Token { kind: '{', .. })) => {
self.toks.next(); self.toks.next();
comment.push_str(&self.parse_interpolation()?.to_css_string(span)?); comment.push_str(
&self
.parse_interpolation()?
.to_css_string(span, self.options.is_compressed())?,
);
continue; continue;
} }
(..) => comment.push(tok.kind), (..) => comment.push(tok.kind),
@ -541,7 +549,7 @@ impl<'a, 'b> Parser<'a, 'b> {
let interpolation = self.parse_interpolation()?; let interpolation = self.parse_interpolation()?;
Ok(match interpolation.node { Ok(match interpolation.node {
Value::String(v, ..) => Cow::owned(v), Value::String(v, ..) => Cow::owned(v),
v => v.to_css_string(interpolation.span)?, v => v.to_css_string(interpolation.span, self.options.is_compressed())?,
}) })
} }
@ -855,7 +863,7 @@ impl<'a, 'b> Parser<'a, 'b> {
return Err(( return Err((
format!( format!(
"compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n", "compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n",
compound.components.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(", ") compound.components.iter().map(ToString::to_string).collect::<Vec<String>>().join(", ")
) )
, self.span_before).into()); , self.span_before).into());
} }
@ -922,7 +930,11 @@ impl<'a, 'b> Parser<'a, 'b> {
self.toks.next(); self.toks.next();
self.span_before = pos; self.span_before = pos;
let interpolation = self.parse_interpolation()?; let interpolation = self.parse_interpolation()?;
params.push_str(&interpolation.node.to_css_string(interpolation.span)?); params.push_str(
&interpolation
.node
.to_css_string(interpolation.span, self.options.is_compressed())?,
);
continue; continue;
} }

View File

@ -197,7 +197,9 @@ impl<'a, 'b> Parser<'a, 'b> {
}; };
let Spanned { node: module, span } = self.parse_quoted_string(quote)?; let Spanned { node: module, span } = self.parse_quoted_string(quote)?;
let module_name = module.unquote().to_css_string(span)?; let module_name = module
.unquote()
.to_css_string(span, self.options.is_compressed())?;
self.whitespace_or_comment(); self.whitespace_or_comment();

View File

@ -20,7 +20,11 @@ impl<'a, 'b> Parser<'a, 'b> {
self.span_before = pos; self.span_before = pos;
self.toks.next(); self.toks.next();
let interpolation = self.parse_interpolation()?; let interpolation = self.parse_interpolation()?;
buf.push_str(&interpolation.node.to_css_string(interpolation.span)?); buf.push_str(
&interpolation
.node
.to_css_string(interpolation.span, self.options.is_compressed())?,
);
} else { } else {
buf.push('#'); buf.push('#');
} }
@ -93,7 +97,10 @@ impl<'a, 'b> Parser<'a, 'b> {
let interpolation = self.parse_interpolation()?; let interpolation = self.parse_interpolation()?;
match interpolation.node { match interpolation.node {
Value::String(ref s, ..) => buf.push_str(s), Value::String(ref s, ..) => buf.push_str(s),
v => buf.push_str(v.to_css_string(interpolation.span)?.borrow()), v => buf.push_str(
v.to_css_string(interpolation.span, self.options.is_compressed())?
.borrow(),
),
}; };
} else { } else {
buf.push('#'); buf.push('#');
@ -142,7 +149,11 @@ impl<'a, 'b> Parser<'a, 'b> {
match kind { match kind {
'+' | '-' | '0'..='9' => { '+' | '-' | '0'..='9' => {
let number = self.parse_dimension(&|_| false)?; let number = self.parse_dimension(&|_| false)?;
buf.push_str(&number.node.to_css_string(number.span)?); buf.push_str(
&number
.node
.to_css_string(number.span, self.options.is_compressed())?,
);
} }
'#' => { '#' => {
self.toks.next(); self.toks.next();
@ -316,7 +327,7 @@ impl<'a, 'b> Parser<'a, 'b> {
q @ ('"' | '\'') => { q @ ('"' | '\'') => {
self.toks.next(); self.toks.next();
let s = self.parse_quoted_string(q)?; let s = self.parse_quoted_string(q)?;
buffer.push_str(&s.node.to_css_string(s.span)?); buffer.push_str(&s.node.to_css_string(s.span, self.options.is_compressed())?);
wrote_newline = false; wrote_newline = false;
} }
'/' => { '/' => {

View File

@ -122,14 +122,26 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
Value::Dimension(Some(-n), u, should_divide) Value::Dimension(Some(-n), u, should_divide)
} }
Value::Dimension(None, u, should_divide) => Value::Dimension(None, u, should_divide), Value::Dimension(None, u, should_divide) => Value::Dimension(None, u, should_divide),
v => Value::String(format!("-{}", v.to_css_string(self.span)?), QuoteKind::None), v => Value::String(
format!(
"-{}",
v.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None,
),
}) })
} }
fn unary_plus(&self, val: Value) -> SassResult<Value> { fn unary_plus(&self, val: Value) -> SassResult<Value> {
Ok(match val { Ok(match val {
v @ Value::Dimension(..) => v, v @ Value::Dimension(..) => v,
v => Value::String(format!("+{}", v.to_css_string(self.span)?), QuoteKind::None), v => Value::String(
format!(
"+{}",
v.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None,
),
}) })
} }
@ -176,28 +188,36 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
} }
Value::True | Value::False => match right { Value::True | Value::False => match right {
Value::String(s, QuoteKind::Quoted) => Value::String( Value::String(s, QuoteKind::Quoted) => Value::String(
format!("{}{}", left.to_css_string(self.span)?, s), format!(
"{}{}",
left.to_css_string(self.span, self.parser.options.is_compressed())?,
s
),
QuoteKind::Quoted, QuoteKind::Quoted,
), ),
_ => Value::String( _ => Value::String(
format!( format!(
"{}{}", "{}{}",
left.to_css_string(self.span)?, left.to_css_string(self.span, self.parser.options.is_compressed())?,
right.to_css_string(self.span)? right.to_css_string(self.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
), ),
}, },
Value::Important => match right { Value::Important => match right {
Value::String(s, ..) => Value::String( Value::String(s, ..) => Value::String(
format!("{}{}", left.to_css_string(self.span)?, s), format!(
"{}{}",
left.to_css_string(self.span, self.parser.options.is_compressed())?,
s
),
QuoteKind::None, QuoteKind::None,
), ),
_ => Value::String( _ => Value::String(
format!( format!(
"{}{}", "{}{}",
left.to_css_string(self.span)?, left.to_css_string(self.span, self.parser.options.is_compressed())?,
right.to_css_string(self.span)? right.to_css_string(self.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
), ),
@ -205,7 +225,9 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
Value::Null => match right { Value::Null => match right {
Value::Null => Value::Null, Value::Null => Value::Null,
_ => Value::String( _ => Value::String(
right.to_css_string(self.span)?.into_owned(), right
.to_css_string(self.span, self.parser.options.is_compressed())?
.into_owned(),
QuoteKind::None, QuoteKind::None,
), ),
}, },
@ -230,14 +252,34 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
Value::Dimension(Some(num + num2.convert(&unit2, &unit)), unit, true) Value::Dimension(Some(num + num2.convert(&unit2, &unit)), unit, true)
} }
} }
Value::String(s, q) => Value::String(format!("{}{}{}", num, unit, s), q), Value::String(s, q) => Value::String(
Value::Null => Value::String(format!("{}{}", num, unit), QuoteKind::None), format!(
"{}{}{}",
num.to_string(self.parser.options.is_compressed()),
unit,
s
),
q,
),
Value::Null => Value::String(
format!(
"{}{}",
num.to_string(self.parser.options.is_compressed()),
unit
),
QuoteKind::None,
),
Value::True Value::True
| Value::False | Value::False
| Value::List(..) | Value::List(..)
| Value::Important | Value::Important
| Value::ArgList(..) => Value::String( | Value::ArgList(..) => Value::String(
format!("{}{}{}", num, unit, right.to_css_string(self.span)?), format!(
"{}{}{}",
num.to_string(self.parser.options.is_compressed()),
unit,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Map(..) | Value::FunctionRef(..) => { Value::Map(..) | Value::FunctionRef(..) => {
@ -251,7 +293,7 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
return Err(( return Err((
format!( format!(
"Undefined operation \"{}{} + {}\".", "Undefined operation \"{}{} + {}\".",
num, num.inspect(),
unit, unit,
right.inspect(self.span)? right.inspect(self.span)?
), ),
@ -264,7 +306,11 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
Value::String(s, q) => Value::String(format!("{}{}", c, s), q), Value::String(s, q) => Value::String(format!("{}{}", c, s), q),
Value::Null => Value::String(c.to_string(), QuoteKind::None), Value::Null => Value::String(c.to_string(), QuoteKind::None),
Value::List(..) => Value::String( Value::List(..) => Value::String(
format!("{}{}", c, right.to_css_string(self.span)?), format!(
"{}{}",
c,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
_ => { _ => {
@ -281,17 +327,25 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
}, },
Value::String(text, quotes) => match right { Value::String(text, quotes) => match right {
Value::String(text2, ..) => Value::String(text + &text2, quotes), Value::String(text2, ..) => Value::String(text + &text2, quotes),
_ => Value::String(text + &right.to_css_string(self.span)?, quotes), _ => Value::String(
text + &right.to_css_string(self.span, self.parser.options.is_compressed())?,
quotes,
),
}, },
Value::List(..) | Value::ArgList(..) => match right { Value::List(..) | Value::ArgList(..) => match right {
Value::String(s, q) => { Value::String(s, q) => Value::String(
Value::String(format!("{}{}", left.to_css_string(self.span)?, s), q) format!(
} "{}{}",
left.to_css_string(self.span, self.parser.options.is_compressed())?,
s
),
q,
),
_ => Value::String( _ => Value::String(
format!( format!(
"{}{}", "{}{}",
left.to_css_string(self.span)?, left.to_css_string(self.span, self.parser.options.is_compressed())?,
right.to_css_string(self.span)? right.to_css_string(self.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
), ),
@ -314,7 +368,10 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
}; };
Ok(match left { Ok(match left {
Value::Null => Value::String( Value::Null => Value::String(
format!("-{}", right.to_css_string(self.span)?), format!(
"-{}",
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
v @ Value::Dimension(None, ..) => v, v @ Value::Dimension(None, ..) => v,
@ -344,7 +401,12 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
| Value::True | Value::True
| Value::False | Value::False
| Value::ArgList(..) => Value::String( | Value::ArgList(..) => Value::String(
format!("{}{}-{}", num, unit, right.to_css_string(self.span)?), format!(
"{}{}-{}",
num.to_string(self.parser.options.is_compressed()),
unit,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Map(..) | Value::FunctionRef(..) => { Value::Map(..) | Value::FunctionRef(..) => {
@ -358,7 +420,7 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
return Err(( return Err((
format!( format!(
"Undefined operation \"{}{} - {}\".", "Undefined operation \"{}{} - {}\".",
num, num.inspect(),
unit, unit,
right.inspect(self.span)? right.inspect(self.span)?
), ),
@ -366,7 +428,14 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
) )
.into()) .into())
} }
Value::Null => Value::String(format!("{}{}-", num, unit), QuoteKind::None), Value::Null => Value::String(
format!(
"{}{}-",
num.to_string(self.parser.options.is_compressed()),
unit
),
QuoteKind::None,
),
}, },
Value::Color(c) => match right { Value::Color(c) => match right {
Value::String(s, q) => { Value::String(s, q) => {
@ -385,32 +454,45 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
.into()) .into())
} }
_ => Value::String( _ => Value::String(
format!("{}-{}", c, right.to_css_string(self.span)?), format!(
"{}-{}",
c,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
}, },
Value::String(..) => Value::String( Value::String(..) => Value::String(
format!( format!(
"{}-{}", "{}-{}",
left.to_css_string(self.span)?, left.to_css_string(self.span, self.parser.options.is_compressed())?,
right.to_css_string(self.span)? right.to_css_string(self.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
), ),
_ => match right { _ => match right {
Value::String(s, q) => Value::String( Value::String(s, q) => Value::String(
format!("{}-{}{}{}", left.to_css_string(self.span)?, q, s, q), format!(
"{}-{}{}{}",
left.to_css_string(self.span, self.parser.options.is_compressed())?,
q,
s,
q
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Null => Value::String( Value::Null => Value::String(
format!("{}-", left.to_css_string(self.span)?), format!(
"{}-",
left.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
_ => Value::String( _ => Value::String(
format!( format!(
"{}-{}", "{}-{}",
left.to_css_string(self.span)?, left.to_css_string(self.span, self.parser.options.is_compressed())?,
right.to_css_string(self.span)? right.to_css_string(self.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
), ),
@ -448,7 +530,7 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
return Err(( return Err((
format!( format!(
"Undefined operation \"{}{} * {}\".", "Undefined operation \"{}{} * {}\".",
num, num.inspect(),
unit, unit,
right.inspect(self.span)? right.inspect(self.span)?
), ),
@ -487,7 +569,10 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
}; };
Ok(match left { Ok(match left {
Value::Null => Value::String( Value::Null => Value::String(
format!("/{}", right.to_css_string(self.span)?), format!(
"/{}",
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Dimension(None, ..) => todo!(), Value::Dimension(None, ..) => todo!(),
@ -537,24 +622,50 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
} }
} else { } else {
Value::String( Value::String(
format!("{}{}/{}{}", num, unit, num2, unit2), format!(
"{}{}/{}{}",
num.to_string(self.parser.options.is_compressed()),
unit,
num2.to_string(self.parser.options.is_compressed()),
unit2
),
QuoteKind::None, QuoteKind::None,
) )
} }
} }
Value::String(s, q) => { Value::String(s, q) => Value::String(
Value::String(format!("{}{}/{}{}{}", num, unit, q, s, q), QuoteKind::None) format!(
} "{}{}/{}{}{}",
num.to_string(self.parser.options.is_compressed()),
unit,
q,
s,
q
),
QuoteKind::None,
),
Value::List(..) Value::List(..)
| Value::True | Value::True
| Value::False | Value::False
| Value::Important | Value::Important
| Value::Color(..) | Value::Color(..)
| Value::ArgList(..) => Value::String( | Value::ArgList(..) => Value::String(
format!("{}{}/{}", num, unit, right.to_css_string(self.span)?), format!(
"{}{}/{}",
num.to_string(self.parser.options.is_compressed()),
unit,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None,
),
Value::Null => Value::String(
format!(
"{}{}/",
num.to_string(self.parser.options.is_compressed()),
unit
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Null => Value::String(format!("{}{}/", num, unit), QuoteKind::None),
Value::Map(..) | Value::FunctionRef(..) => { Value::Map(..) | Value::FunctionRef(..) => {
return Err(( return Err((
format!("{} isn't a valid CSS value.", right.inspect(self.span)?), format!("{} isn't a valid CSS value.", right.inspect(self.span)?),
@ -580,7 +691,11 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
.into()) .into())
} }
_ => Value::String( _ => Value::String(
format!("{}/{}", c, right.to_css_string(self.span)?), format!(
"{}/{}",
c,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
}, },
@ -596,7 +711,13 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
| Value::Color(..) | Value::Color(..)
| Value::List(..) | Value::List(..)
| Value::ArgList(..) => Value::String( | Value::ArgList(..) => Value::String(
format!("{}{}{}/{}", q1, s1, q1, right.to_css_string(self.span)?), format!(
"{}{}{}/{}",
q1,
s1,
q1,
right.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Null => Value::String(format!("{}{}{}/", q1, s1, q1), QuoteKind::None), Value::Null => Value::String(format!("{}{}{}/", q1, s1, q1), QuoteKind::None),
@ -610,18 +731,27 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
}, },
_ => match right { _ => match right {
Value::String(s, q) => Value::String( Value::String(s, q) => Value::String(
format!("{}/{}{}{}", left.to_css_string(self.span)?, q, s, q), format!(
"{}/{}{}{}",
left.to_css_string(self.span, self.parser.options.is_compressed())?,
q,
s,
q
),
QuoteKind::None, QuoteKind::None,
), ),
Value::Null => Value::String( Value::Null => Value::String(
format!("{}/", left.to_css_string(self.span)?), format!(
"{}/",
left.to_css_string(self.span, self.parser.options.is_compressed())?
),
QuoteKind::None, QuoteKind::None,
), ),
_ => Value::String( _ => Value::String(
format!( format!(
"{}/{}", "{}/{}",
left.to_css_string(self.span)?, left.to_css_string(self.span, self.parser.options.is_compressed())?,
right.to_css_string(self.span)? right.to_css_string(self.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
), ),

View File

@ -272,9 +272,17 @@ impl<'a, 'b> Parser<'a, 'b> {
} }
"url" => match self.try_parse_url()? { "url" => match self.try_parse_url()? {
Some(val) => s = val, Some(val) => s = val,
None => s.push_str(&self.parse_call_args()?.to_css_string()?), None => s.push_str(
&self
.parse_call_args()?
.to_css_string(self.options.is_compressed())?,
),
}, },
_ => s.push_str(&self.parse_call_args()?.to_css_string()?), _ => s.push_str(
&self
.parse_call_args()?
.to_css_string(self.options.is_compressed())?,
),
} }
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
@ -1142,7 +1150,10 @@ impl<'a, 'b: 'a, 'c> IntermediateValueIterator<'a, 'b, 'c> {
"/{}", "/{}",
ValueVisitor::new(self.parser, right.span) ValueVisitor::new(self.parser, right.span)
.eval(right.node, false)? .eval(right.node, false)?
.to_css_string(right.span)? .to_css_string(
right.span,
self.parser.options.is_compressed()
)?
), ),
QuoteKind::None, QuoteKind::None,
)), )),
@ -1348,7 +1359,7 @@ impl<'a, 'b: 'a, 'c> IntermediateValueIterator<'a, 'b, 'c> {
"/{}", "/{}",
ValueVisitor::new(self.parser, val.span) ValueVisitor::new(self.parser, val.span)
.eval(val.node, false)? .eval(val.node, false)?
.to_css_string(val.span)? .to_css_string(val.span, self.parser.options.is_compressed())?
), ),
QuoteKind::None, QuoteKind::None,
)), )),

View File

@ -189,7 +189,7 @@ impl Display for Attribute {
// (also avoids the clone because we can consume/modify self) // (also avoids the clone because we can consume/modify self)
f.write_str( f.write_str(
&Value::String(self.value.clone(), QuoteKind::Quoted) &Value::String(self.value.clone(), QuoteKind::Quoted)
.to_css_string(self.span) .to_css_string(self.span, false)
.unwrap(), .unwrap(),
)?; )?;
// todo: this space is not emitted when `compressed` output // todo: this space is not emitted when `compressed` output

View File

@ -14,7 +14,7 @@ impl Style {
Ok(format!( Ok(format!(
"{}: {};", "{}: {};",
self.property, self.property,
self.value.node.to_css_string(self.value.span)? self.value.node.to_css_string(self.value.span, false)?
)) ))
} }
} }

View File

@ -205,22 +205,28 @@ impl Value {
} }
} }
pub fn to_css_string(&self, span: Span) -> SassResult<Cow<'static, str>> { pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult<Cow<'static, str>> {
Ok(match self { Ok(match self {
Value::Important => Cow::const_str("!important"), Value::Important => Cow::const_str("!important"),
Value::Dimension(num, unit, _) => match unit { Value::Dimension(num, unit, _) => match unit {
Unit::Mul(..) | Unit::Div(..) => { Unit::Mul(..) | Unit::Div(..) => {
if let Some(num) = num { if let Some(num) = num {
return Err( return Err((
(format!("{}{} isn't a valid CSS value.", num, unit), span).into() format!(
); "{}{} isn't a valid CSS value.",
num.to_string(is_compressed),
unit
),
span,
)
.into());
} }
return Err((format!("NaN{} isn't a valid CSS value.", unit), span).into()); return Err((format!("NaN{} isn't a valid CSS value.", unit), span).into());
} }
_ => { _ => {
if let Some(num) = num { if let Some(num) = num {
Cow::owned(format!("{}{}", num, unit)) Cow::owned(format!("{}{}", num.to_string(is_compressed), unit))
} else { } else {
Cow::owned(format!("NaN{}", unit)) Cow::owned(format!("NaN{}", unit))
} }
@ -237,17 +243,25 @@ impl Value {
Brackets::None => Cow::owned( Brackets::None => Cow::owned(
vals.iter() vals.iter()
.filter(|x| !x.is_null()) .filter(|x| !x.is_null())
.map(|x| x.to_css_string(span)) .map(|x| x.to_css_string(span, is_compressed))
.collect::<SassResult<Vec<Cow<'static, str>>>>()? .collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(sep.as_str()), .join(if is_compressed {
sep.as_compressed_str()
} else {
sep.as_str()
}),
), ),
Brackets::Bracketed => Cow::owned(format!( Brackets::Bracketed => Cow::owned(format!(
"[{}]", "[{}]",
vals.iter() vals.iter()
.filter(|x| !x.is_null()) .filter(|x| !x.is_null())
.map(|x| x.to_css_string(span)) .map(|x| x.to_css_string(span, is_compressed))
.collect::<SassResult<Vec<Cow<'static, str>>>>()? .collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(sep.as_str()), .join(if is_compressed {
sep.as_compressed_str()
} else {
sep.as_str()
}),
)), )),
}, },
Value::Color(c) => Cow::owned(c.to_string()), Value::Color(c) => Cow::owned(c.to_string()),
@ -287,9 +301,13 @@ impl Value {
Value::ArgList(args) => Cow::owned( Value::ArgList(args) => Cow::owned(
args.iter() args.iter()
.filter(|x| !x.is_null()) .filter(|x| !x.is_null())
.map(|a| a.node.to_css_string(span)) .map(|a| a.node.to_css_string(span, is_compressed))
.collect::<SassResult<Vec<Cow<'static, str>>>>()? .collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(", "), .join(if is_compressed {
ListSeparator::Comma.as_compressed_str()
} else {
ListSeparator::Comma.as_str()
}),
), ),
}) })
} }
@ -466,7 +484,9 @@ impl Value {
.collect::<SassResult<Vec<String>>>()? .collect::<SassResult<Vec<String>>>()?
.join(", ") .join(", ")
)), )),
Value::Dimension(Some(num), unit, _) => Cow::owned(format!("{}{}", num, unit)), Value::Dimension(Some(num), unit, _) => {
Cow::owned(format!("{}{}", num.inspect(), unit))
}
Value::Dimension(None, unit, ..) => Cow::owned(format!("NaN{}", unit)), Value::Dimension(None, unit, ..) => Cow::owned(format!("NaN{}", unit)),
Value::ArgList(args) if args.is_empty() => Cow::const_str("()"), Value::ArgList(args) if args.is_empty() => Cow::const_str("()"),
Value::ArgList(args) if args.len() == 1 => Cow::owned(format!( Value::ArgList(args) if args.len() == 1 => Cow::owned(format!(
@ -488,7 +508,7 @@ impl Value {
| Value::True | Value::True
| Value::False | Value::False
| Value::Color(..) | Value::Color(..)
| Value::String(..) => self.to_css_string(span)?, | Value::String(..) => self.to_css_string(span, false)?,
}) })
} }

View File

@ -1,8 +1,7 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
convert::{From, TryFrom}, convert::{From, TryFrom},
fmt::{self, Display, Write}, fmt, mem,
mem,
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign}, ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign},
}; };
@ -313,8 +312,8 @@ from_smaller_integer!(u8);
impl fmt::Debug for Number { impl fmt::Debug for Number {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Small(..) => write!(f, "Number::Small( {} )", self), Self::Small(..) => write!(f, "Number::Small( {} )", self.to_string(false)),
Self::Big(..) => write!(f, "Number::Big( {} )", self), Self::Big(..) => write!(f, "Number::Big( {} )", self.to_string(false)),
} }
} }
} }
@ -355,16 +354,24 @@ impl ToPrimitive for Number {
} }
} }
impl Display for Number { impl Number {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { pub(crate) fn inspect(&self) -> String {
self.to_string(false)
}
pub(crate) fn to_string(&self, is_compressed: bool) -> String {
let mut whole = self.to_integer().abs(); let mut whole = self.to_integer().abs();
let has_decimal = self.is_decimal(); let has_decimal = self.is_decimal();
let mut frac = self.abs().fract(); let mut frac = self.abs().fract();
let mut dec = String::with_capacity(if has_decimal { PRECISION } else { 0 }); let mut dec = String::with_capacity(if has_decimal { PRECISION } else { 0 });
let mut buf = String::new();
if has_decimal { if has_decimal {
for _ in 0..(PRECISION - 1) { for _ in 0..(PRECISION - 1) {
frac *= 10_i64; frac *= 10_i64;
write!(dec, "{}", frac.to_integer())?; dec.push_str(&frac.to_integer().to_string());
frac = frac.fract(); frac = frac.fract();
if frac.is_zero() { if frac.is_zero() {
break; break;
@ -398,20 +405,25 @@ impl Display for Number {
} }
} }
} else { } else {
write!(dec, "{}", end)?; dec.push_str(&end.to_string());
} }
} }
} }
if self.is_negative() && (!whole.is_zero() || !dec.is_empty()) { if self.is_negative() && (!whole.is_zero() || !dec.is_empty()) {
f.write_char('-')?; buf.push('-');
} }
write!(f, "{}", whole)?;
if !(whole.is_zero() && is_compressed) {
buf.push_str(&whole.to_string());
}
if !dec.is_empty() { if !dec.is_empty() {
f.write_char('.')?; buf.push('.');
write!(f, "{}", dec)?; buf.push_str(&dec);
} }
Ok(())
buf
} }
} }

View File

@ -13,13 +13,6 @@ test!(
"a{color:red;color:green;color:blue}", "a{color:red;color:green;color:blue}",
grass::Options::default().style(grass::OutputStyle::Compressed) grass::Options::default().style(grass::OutputStyle::Compressed)
); );
test!(
#[ignore = "we don't support compressed values"]
strips_the_leading_zero,
"a {\n color: 0.5;\n}\n",
"a{color:.5}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);
test!( test!(
compresses_media_rule, compresses_media_rule,
"@media foo {\n a {\n color: red;\n }\n}\n", "@media foo {\n a {\n color: red;\n }\n}\n",
@ -86,3 +79,40 @@ test!(
"a{color:red}b{color:green}", "a{color:red}b{color:green}",
grass::Options::default().style(grass::OutputStyle::Compressed) grass::Options::default().style(grass::OutputStyle::Compressed)
); );
test!(
removes_spaces_in_comma_separated_list,
"a {\n color: a, b, c;\n}\n",
"a{color:a,b,c}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);
test!(
removes_leading_zero_in_number_under_1,
"a {\n color: 0.5;\n}\n",
"a{color:.5}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);
test!(
#[ignore = "we do not support compressed colors"]
removes_leading_zero_in_number_under_1_in_rgba_alpha_channel,
"a {\n color: rgba(1, 1, 1, 0.5);\n}\n",
"a{color:rgba(1,1,1,.5)}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);
test!(
retains_leading_zero_in_opacity,
"a {\n color: opacity(0.5);\n}\n",
"a{color:opacity(0.5)}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);
test!(
retains_leading_zero_in_saturate,
"a {\n color: saturate(0.5);\n}\n",
"a{color:saturate(0.5)}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);
test!(
retains_leading_zero_in_grayscale,
"a {\n color: grayscale(0.5);\n}\n",
"a{color:grayscale(0.5)}",
grass::Options::default().style(grass::OutputStyle::Compressed)
);