unify serialization and inspection

This commit is contained in:
connorskees 2023-01-07 04:17:05 +00:00
parent d8867e42db
commit d14e6bd9f7
15 changed files with 472 additions and 345 deletions

View File

@ -10,10 +10,13 @@
- add `grass::include!` macro to make it easier to include CSS at compile time
- improve error message for complex units in calculations
- more accurate formatting of named arguments in arglists when passed to `inspect(..)`
- more accurate formatting of nested lists with different separators when passed to `inspect(..)`
- support `$whiteness` and `$blackness` as arguments to `scale-color(..)`
- more accurate list separator from `join(..)`
- resolve unicode edge cases in `str-index(..)`
- more robust support for `@forward` prefixes
- allow strings as the first argument to `call(..)`
- bug fix: add back support for the `$css` argument to `get-function(..)`. regressed in 0.12.0
# 0.12.0

View File

@ -81,8 +81,8 @@ Using a modified version of the spec runner that ignores warnings and error span
```
2022-01-03
PASSING: 6118
FAILING: 787
PASSING: 6153
FAILING: 752
TOTAL: 6905
```

View File

@ -56,7 +56,7 @@ impl CssStmt {
CssStmt::RuleSet { selector, body, .. } => {
selector.is_invisible() || body.iter().all(CssStmt::is_invisible)
}
CssStmt::Style(style) => style.value.node.is_null(),
CssStmt::Style(style) => style.value.node.is_blank(),
CssStmt::Media(media_rule, ..) => media_rule.body.iter().all(CssStmt::is_invisible),
CssStmt::UnknownAtRule(..) | CssStmt::Import(..) | CssStmt::Comment(..) => false,
CssStmt::Supports(supports_rule, ..) => {

View File

@ -90,9 +90,7 @@ pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassR
args.max_args(1)?;
Ok(match args.get_err(0, "number")? {
Value::Dimension(SassNumber {
num: _,
unit: Unit::None,
as_slash: _,
unit: Unit::None, ..
}) => Value::True,
Value::Dimension(SassNumber { .. }) => Value::False,
v => {
@ -108,7 +106,7 @@ pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassR
pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
Ok(Value::String(
args.get_err(0, "value")?.inspect(args.span())?.into_owned(),
args.get_err(0, "value")?.inspect(args.span())?,
QuoteKind::None,
))
}
@ -134,16 +132,11 @@ pub(crate) fn global_variable_exists(
) -> SassResult<Value> {
args.max_args(2)?;
let name: Identifier = match args.get_err(0, "name")? {
Value::String(s, _) => s.into(),
v => {
return Err((
format!("$name: {} is not a string.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let name = Identifier::from(
args.get_err(0, "name")?
.assert_string_with_name("name", args.span())?
.0,
);
let module = match args.default_arg(1, "module", Value::Null) {
Value::String(s, _) => Some(s),
@ -269,15 +262,17 @@ pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> S
}
};
let func = if let Some(module_name) = module {
if css {
return Err((
"$css and $module may not both be passed at once.",
args.span(),
)
.into());
}
if css && module.is_some() {
return Err((
"$css and $module may not both be passed at once.",
args.span(),
)
.into());
}
let func = if css {
Some(SassFunction::Plain { name })
} else if let Some(module_name) = module {
visitor.env.get_fn(
name,
Some(Spanned {
@ -303,7 +298,18 @@ pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> S
pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span();
let func = match args.get_err(0, "function")? {
Value::FunctionRef(f) => f,
Value::FunctionRef(f) => *f,
Value::String(name, ..) => {
let name = Identifier::from(name);
match visitor.env.get_fn(name, None)? {
Some(f) => f,
None => match GLOBAL_FUNCTIONS.get(name.as_str()) {
Some(f) => SassFunction::Builtin(f.clone(), name),
None => SassFunction::Plain { name },
},
}
}
v => {
return Err((
format!(
@ -318,7 +324,7 @@ pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResul
args.remove_positional(0);
visitor.run_function_callable_with_maybe_evaled(*func, MaybeEvaledArguments::Evaled(args), span)
visitor.run_function_callable_with_maybe_evaled(func, MaybeEvaledArguments::Evaled(args), span)
}
#[allow(clippy::needless_pass_by_value)]

View File

@ -115,14 +115,6 @@ impl ListSeparator {
}
}
pub fn as_compressed_str(self) -> &'static str {
match self {
Self::Space | Self::Undecided => " ",
Self::Comma => ",",
Self::Slash => "/",
}
}
pub fn name(self) -> &'static str {
match self {
Self::Space | Self::Undecided => "space",

View File

@ -62,9 +62,7 @@ pub(crate) fn add(left: Value, right: Value, options: &Options, span: Span) -> S
Value::Null => match right {
Value::Null => Value::Null,
_ => Value::String(
right
.to_css_string(span, options.is_compressed())?
.into_owned(),
right.to_css_string(span, options.is_compressed())?,
QuoteKind::None,
),
},

View File

@ -1201,8 +1201,7 @@ impl<'a> Visitor<'a> {
fn visit_error_rule(&mut self, error_rule: AstErrorRule) -> SassResult<Box<SassError>> {
let value = self
.visit_expr(error_rule.value)?
.inspect(error_rule.span)?
.into_owned();
.inspect(error_rule.span)?;
Ok((value, error_rule.span).into())
}
@ -2268,12 +2267,31 @@ impl<'a> Visitor<'a> {
Err(("Function finished without @return.", span).into())
}),
SassFunction::Plain { name } => {
let has_named;
let mut rest = None;
// todo: somewhat hacky solution to support plain css fns passed
// as strings to `call(..)`
let arguments = match arguments {
MaybeEvaledArguments::Invocation(args) => args,
MaybeEvaledArguments::Evaled(..) => unreachable!(),
MaybeEvaledArguments::Invocation(args) => {
has_named = !args.named.is_empty() || args.keyword_rest.is_some();
rest = args.rest;
args.positional
.into_iter()
.map(|arg| self.evaluate_to_css(arg, QuoteKind::Quoted, span))
.collect::<SassResult<Vec<_>>>()?
}
MaybeEvaledArguments::Evaled(args) => {
has_named = !args.named.is_empty();
args.positional
.into_iter()
.map(|arg| Ok(arg.to_css_string(span, self.options.is_compressed())?))
.collect::<SassResult<Vec<_>>>()?
}
};
if !arguments.named.is_empty() || arguments.keyword_rest.is_some() {
if has_named {
return Err(
("Plain CSS functions don't support keyword arguments.", span).into(),
);
@ -2282,17 +2300,17 @@ impl<'a> Visitor<'a> {
let mut buffer = format!("{}(", name.as_str());
let mut first = true;
for argument in arguments.positional {
for argument in arguments {
if first {
first = false;
} else {
buffer.push_str(", ");
}
buffer.push_str(&self.evaluate_to_css(argument, QuoteKind::Quoted, span)?);
buffer.push_str(&argument);
}
if let Some(rest_arg) = arguments.rest {
if let Some(rest_arg) = rest {
let rest = self.visit_expr(rest_arg)?;
if !first {
buffer.push_str(", ");
@ -2709,9 +2727,8 @@ impl<'a> Visitor<'a> {
let left_is_number = matches!(left, Value::Dimension { .. });
let right_is_number = matches!(right, Value::Dimension { .. });
let result = div(left.clone(), right.clone(), self.options, span)?;
if left_is_number && right_is_number && allows_slash {
let result = div(left.clone(), right.clone(), self.options, span)?;
return result.with_slash(
left.assert_number(span)?,
right.assert_number(span)?,
@ -2727,6 +2744,8 @@ impl<'a> Visitor<'a> {
// );
}
let result = div(left, right, self.options, span)?;
result
}
BinaryOp::Rem => {
@ -2742,9 +2761,7 @@ impl<'a> Visitor<'a> {
expr = expr.unquote();
}
Ok(expr
.to_css_string(span, self.options.is_compressed())?
.into_owned())
expr.to_css_string(span, self.options.is_compressed())
}
pub fn visit_ruleset(&mut self, ruleset: AstRuleSet) -> SassResult<Option<Value>> {
@ -2906,7 +2923,7 @@ impl<'a> Visitor<'a> {
{
// If the value is an empty list, preserve it, because converting it to CSS
// will throw an error that we want the user to see.
if !value.is_null() || value.is_empty_list() {
if !value.is_blank() || value.is_empty_list() {
// todo: superfluous clones?
self.css_tree.add_stmt(
CssStmt::Style(Style {

View File

@ -97,6 +97,11 @@ impl<'a> Iterator for Lexer<'a> {
tok
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.buf.len() - self.cursor;
(remaining, Some(remaining))
}
}
struct TokenLexer<'a> {
@ -127,6 +132,10 @@ impl<'a> Iterator for TokenLexer<'a> {
self.cursor += len;
Some(Token { pos, kind })
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.buf.size_hint()
}
}
impl<'a> Lexer<'a> {

View File

@ -1,29 +1,24 @@
use std::io::Write;
use codemap::{CodeMap, Span, Spanned};
use codemap::{CodeMap, Span};
use crate::{
ast::{CssStmt, MediaQuery, Style, SupportsRule},
color::{Color, ColorFormat, NAMED_COLORS},
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
selector::{
Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo,
SelectorList, SimpleSelector,
},
utils::hex_char_for,
value::{fuzzy_equals, CalculationArg, SassCalculation, SassNumber, Value},
value::{
fuzzy_equals, ArgList, CalculationArg, SassCalculation, SassFunction, SassMap, SassNumber,
Value,
},
Options,
};
pub(crate) fn serialize_color(color: &Color, options: &Options, span: Span) -> String {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, false, span);
serializer.visit_color(color);
serializer.finish_for_expr()
}
pub(crate) fn serialize_selector_list(
list: &SelectorList,
options: &Options,
@ -37,19 +32,6 @@ pub(crate) fn serialize_selector_list(
serializer.finish_for_expr()
}
pub(crate) fn serialize_calculation(
calculation: &SassCalculation,
options: &Options,
span: Span,
) -> SassResult<String> {
let map = CodeMap::new();
let mut serializer = Serializer::new(options, &map, false, span);
serializer.visit_calculation(calculation)?;
Ok(serializer.finish_for_expr())
}
pub(crate) fn serialize_calculation_arg(
arg: &CalculationArg,
options: &Options,
@ -76,6 +58,24 @@ pub(crate) fn serialize_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);
@ -85,6 +85,28 @@ pub(crate) fn inspect_float(number: f64, options: &Options, span: Span) -> Strin
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,
@ -627,17 +649,296 @@ impl<'a> Serializer<'a> {
}
}
fn visit_value(&mut self, value: Spanned<Value>) -> SassResult<()> {
match value.node {
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)?,
_ => {
let value_as_str = value
.node
.to_css_string(value.span, self.options.is_compressed())?;
self.buffer.extend_from_slice(value_as_str.as_bytes());
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(())
@ -657,7 +958,7 @@ impl<'a> Serializer<'a> {
self.buffer.push(b' ');
}
self.visit_value(*style.value)?;
self.visit_value(&style.value.node, style.value.span)?;
Ok(())
}

View File

@ -51,8 +51,8 @@ impl ArgList {
self.len() == 0
}
pub fn is_null(&self) -> bool {
!self.is_empty() && (self.elems.iter().all(Value::is_null))
pub fn is_blank(&self) -> bool {
!self.is_empty() && (self.elems.iter().all(Value::is_blank))
}
pub fn keywords(&self) -> &BTreeMap<Identifier, Value> {

View File

@ -1,4 +1,4 @@
use std::{borrow::Cow, cmp::Ordering};
use std::cmp::Ordering;
use codemap::{Span, Spanned};
@ -8,9 +8,9 @@ use crate::{
error::SassResult,
evaluate::Visitor,
selector::Selector,
serializer::{inspect_number, serialize_calculation, serialize_color, serialize_number},
serializer::{inspect_value, serialize_value},
unit::Unit,
utils::{hex_char_for, is_special_function},
utils::is_special_function,
Options, OutputStyle,
};
@ -121,72 +121,6 @@ impl PartialEq for Value {
impl Eq for Value {}
fn visit_quoted_string(buf: &mut String, force_double_quote: bool, string: &str) {
let mut has_single_quote = false;
let mut has_double_quote = false;
let mut buffer = String::new();
if force_double_quote {
buffer.push('"');
}
let mut iter = string.chars().peekable();
while let Some(c) = iter.next() {
match c {
'\'' => {
if force_double_quote {
buffer.push('\'');
} else if has_double_quote {
return visit_quoted_string(buf, true, string);
} else {
has_single_quote = true;
buffer.push('\'');
}
}
'"' => {
if force_double_quote {
buffer.push('\\');
buffer.push('"');
} else if has_single_quote {
return visit_quoted_string(buf, true, string);
} else {
has_double_quote = true;
buffer.push('"');
}
}
'\x00'..='\x08' | '\x0A'..='\x1F' => {
buffer.push('\\');
if c as u32 > 0xF {
buffer.push(hex_char_for(c as u32 >> 4));
}
buffer.push(hex_char_for(c as u32 & 0xF));
let next = match iter.peek() {
Some(v) => v,
None => break,
};
if next.is_ascii_hexdigit() || next == &' ' || next == &'\t' {
buffer.push(' ');
}
}
'\\' => {
buffer.push('\\');
buffer.push('\\');
}
_ => buffer.push(c),
}
}
if force_double_quote {
buffer.push('"');
} else {
let quote = if has_double_quote { '\'' } else { '"' };
buffer = format!("{}{}{}", quote, buffer, quote);
}
buf.push_str(&buffer);
}
impl Value {
pub fn with_slash(
self,
@ -255,14 +189,13 @@ impl Value {
}
}
// todo: rename is_blank
pub fn is_null(&self) -> bool {
pub fn is_blank(&self) -> bool {
match self {
Value::Null => true,
Value::String(i, QuoteKind::None) if i.is_empty() => true,
Value::List(_, _, Brackets::Bracketed) => false,
Value::List(v, ..) => v.iter().map(Value::is_null).all(|f| f),
Value::ArgList(v, ..) => v.is_null(),
Value::List(v, ..) => v.iter().map(Value::is_blank).all(|f| f),
Value::ArgList(v, ..) => v.is_blank(),
_ => false,
}
}
@ -276,113 +209,20 @@ impl Value {
}
}
pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult<Cow<'static, str>> {
Ok(match self {
Value::Calculation(calc) => Cow::Owned(serialize_calculation(
calc,
&Options::default().style(if is_compressed {
OutputStyle::Compressed
} else {
OutputStyle::Expanded
}),
span,
)?),
Value::Dimension(n) => Cow::Owned(serialize_number(
n,
&Options::default().style(if is_compressed {
OutputStyle::Compressed
} else {
OutputStyle::Expanded
}),
span,
)?),
Value::Map(..) | Value::FunctionRef(..) => {
return Err((
format!("{} isn't a valid CSS value.", self.inspect(span)?),
span,
)
.into())
}
Value::List(vals, sep, brackets) => match brackets {
Brackets::None => Cow::Owned(
vals.iter()
.filter(|x| !x.is_null())
.map(|x| x.to_css_string(span, is_compressed))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(if is_compressed {
sep.as_compressed_str()
} else {
sep.as_str()
}),
),
Brackets::Bracketed => Cow::Owned(format!(
"[{}]",
vals.iter()
.filter(|x| !x.is_null())
.map(|x| x.to_css_string(span, is_compressed))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(if is_compressed {
sep.as_compressed_str()
} else {
sep.as_str()
}),
)),
},
Value::Color(c) => Cow::Owned(serialize_color(
c,
&Options::default().style(if is_compressed {
OutputStyle::Compressed
} else {
OutputStyle::Expanded
}),
span,
)),
Value::String(string, QuoteKind::None) => {
let mut after_newline = false;
let mut buf = String::with_capacity(string.len());
for c in string.chars() {
match c {
'\n' => {
buf.push(' ');
after_newline = true;
}
' ' => {
if !after_newline {
buf.push(' ');
}
}
_ => {
buf.push(c);
after_newline = false;
}
}
}
Cow::Owned(buf)
}
Value::String(string, QuoteKind::Quoted) => {
let mut buf = String::with_capacity(string.len());
visit_quoted_string(&mut buf, false, string);
Cow::Owned(buf)
}
Value::True => Cow::Borrowed("true"),
Value::False => Cow::Borrowed("false"),
Value::Null => Cow::Borrowed(""),
Value::ArgList(args) if args.is_empty() => {
return Err(("() isn't a valid CSS value.", span).into());
}
Value::ArgList(args) => Cow::Owned(
args.elems
.iter()
.filter(|x| !x.is_null())
.map(|a| a.to_css_string(span, is_compressed))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(if is_compressed {
ListSeparator::Comma.as_compressed_str()
} else {
ListSeparator::Comma.as_str()
}),
),
})
pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult<String> {
serialize_value(
self,
&Options::default().style(if is_compressed {
OutputStyle::Compressed
} else {
OutputStyle::Expanded
}),
span,
)
}
pub fn inspect(&self, span: Span) -> SassResult<String> {
inspect_value(self, &Options::default(), span)
}
pub fn is_true(&self) -> bool {
@ -568,80 +408,6 @@ impl Value {
}
}
// TODO:
// https://github.com/sass/dart-sass/blob/d4adea7569832f10e3a26d0e420ae51640740cfb/lib/src/ast/sass/expression/list.dart#L39
// todo: is this actually fallible?
pub fn inspect(&self, span: Span) -> SassResult<Cow<'static, str>> {
Ok(match self {
Value::Calculation(calc) => {
Cow::Owned(serialize_calculation(calc, &Options::default(), span)?)
}
Value::List(v, _, brackets) if v.is_empty() => match brackets {
Brackets::None => Cow::Borrowed("()"),
Brackets::Bracketed => Cow::Borrowed("[]"),
},
Value::List(v, sep, brackets) if v.len() == 1 => match brackets {
Brackets::None => match sep {
ListSeparator::Space | ListSeparator::Slash | ListSeparator::Undecided => {
v[0].inspect(span)?
}
ListSeparator::Comma => Cow::Owned(format!("({},)", v[0].inspect(span)?)),
},
Brackets::Bracketed => match sep {
ListSeparator::Space | ListSeparator::Slash | ListSeparator::Undecided => {
Cow::Owned(format!("[{}]", v[0].inspect(span)?))
}
ListSeparator::Comma => Cow::Owned(format!("[{},]", v[0].inspect(span)?)),
},
},
Value::List(vals, sep, brackets) => Cow::Owned(match brackets {
Brackets::None => vals
.iter()
.map(|x| x.inspect(span))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(sep.as_str()),
Brackets::Bracketed => format!(
"[{}]",
vals.iter()
.map(|x| x.inspect(span))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(sep.as_str()),
),
}),
Value::FunctionRef(f) => Cow::Owned(format!("get-function(\"{}\")", f.name())),
Value::Null => Cow::Borrowed("null"),
Value::Map(map) => Cow::Owned(format!(
"({})",
map.iter()
.map(|(k, v)| Ok(format!("{}: {}", k.inspect(span)?, v.inspect(span)?)))
.collect::<SassResult<Vec<String>>>()?
.join(", ")
)),
Value::Dimension(n) => Cow::Owned(inspect_number(n, &Options::default(), span)?),
Value::ArgList(args) if args.elems.is_empty() => Cow::Borrowed("()"),
Value::ArgList(args) if args.elems.len() == 1 => Cow::Owned(format!(
"({},)",
args.elems
.iter()
.filter(|x| !x.is_null())
.map(|a| a.inspect(span))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(", "),
)),
Value::ArgList(args) => Cow::Owned(
args.elems
.iter()
.filter(|x| !x.is_null())
.map(|a| a.inspect(span))
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(", "),
),
Value::True | Value::False | Value::Color(..) | Value::String(..) => {
self.to_css_string(span, false)?
}
})
}
pub fn as_list(self) -> Vec<Value> {
match self {
Value::List(v, ..) => v,

View File

@ -7,8 +7,8 @@ mod macros;
#[test]
fn null_fs_cannot_import() {
let input = "@import \"foo\";";
tempfile!("foo.scss", "");
let input = "@import \"__foo\";";
tempfile!("__foo.scss", "");
match grass::from_string(
input.to_string(),
&grass::Options::default().fs(&grass::NullFs),

View File

@ -82,7 +82,6 @@ test!(
"a {\n color: (), ();\n}\n"
);
test!(
#[ignore]
inspect_comma_separated_list_of_comma_separated_lists,
"a {\n color: inspect([(1, 2), (3, 4)]);\n}\n",
"a {\n color: [(1, 2), (3, 4)];\n}\n"
@ -98,7 +97,7 @@ test!(
"a {\n color: 1 2 3;\n}\n"
);
test!(
#[ignore]
#[ignore = "we don't support multiple arguments to inspect"]
inspect_comma_list,
"a {\n color: inspect(1, 2, 3)\n}\n",
"a {\n color: 1, 2, 3;\n}\n"

View File

@ -474,3 +474,8 @@ error!(
"a {\n color: set-nth([], 1px, a);\n}\n",
"Error: $n: Invalid index 1px for a list with 0 elements."
);
error!(
#[ignore = ""]
empty_list_is_invalid,
"a {\n color: ();\n}\n", "Error: () isn't a valid CSS value."
);

View File

@ -71,7 +71,6 @@ test!(
"a {\n color: feature-exists(units-level-3)\n}\n",
"a {\n color: true;\n}\n"
);
// Unignore as more features are added
test!(
feature_exists_custom_property,
"a {\n color: feature-exists(custom-property)\n}\n",
@ -328,5 +327,37 @@ test!(
}",
"a {\n color: 255;\n}\n"
);
test!(
call_function_is_string_and_exists,
"a {
color: call(\"red\", blue);
}",
"a {\n color: 0;\n}\n"
);
test!(
call_function_is_string_and_dne,
"a {
color: call(\"reddd\", blue);
}",
"a {\n color: reddd(blue);\n}\n"
);
test!(
call_function_is_string_and_is_user_defined,
"@function foo() {
@return 5;
}
a {
color: call(\"foo\");
}",
"a {\n color: 5;\n}\n"
);
test!(
get_function_css_parameter,
"a {
color: inspect(get-function('empty', $css: true));
}",
"a {\n color: get-function(\"empty\");\n}\n"
);
// todo: if() with different combinations of named and positional args