diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b2694..51d229a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 1b4f9ac..b646bd8 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/grass_internal/src/ast/css.rs b/grass_internal/src/ast/css.rs index 54e4874..59efcc5 100644 --- a/grass_internal/src/ast/css.rs +++ b/grass_internal/src/ast/css.rs @@ -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, ..) => { diff --git a/grass_internal/src/builtin/functions/meta.rs b/grass_internal/src/builtin/functions/meta.rs index 2c39bc2..31851ab 100644 --- a/grass_internal/src/builtin/functions/meta.rs +++ b/grass_internal/src/builtin/functions/meta.rs @@ -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 { 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 { 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 { 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)] diff --git a/grass_internal/src/common.rs b/grass_internal/src/common.rs index 9394c2b..a033397 100644 --- a/grass_internal/src/common.rs +++ b/grass_internal/src/common.rs @@ -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", diff --git a/grass_internal/src/evaluate/bin_op.rs b/grass_internal/src/evaluate/bin_op.rs index 31f6bd5..c773b43 100644 --- a/grass_internal/src/evaluate/bin_op.rs +++ b/grass_internal/src/evaluate/bin_op.rs @@ -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, ), }, diff --git a/grass_internal/src/evaluate/visitor.rs b/grass_internal/src/evaluate/visitor.rs index bf15e55..dcfad17 100644 --- a/grass_internal/src/evaluate/visitor.rs +++ b/grass_internal/src/evaluate/visitor.rs @@ -1201,8 +1201,7 @@ impl<'a> Visitor<'a> { fn visit_error_rule(&mut self, error_rule: AstErrorRule) -> SassResult> { 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::>>()? + } + 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::>>()? + } }; - 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> { @@ -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 { diff --git a/grass_internal/src/lexer.rs b/grass_internal/src/lexer.rs index 067dec0..a85c205 100644 --- a/grass_internal/src/lexer.rs +++ b/grass_internal/src/lexer.rs @@ -97,6 +97,11 @@ impl<'a> Iterator for Lexer<'a> { tok }) } + + fn size_hint(&self) -> (usize, Option) { + 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) { + self.buf.size_hint() + } } impl<'a> Lexer<'a> { diff --git a/grass_internal/src/serializer.rs b/grass_internal/src/serializer.rs index 9866054..2bb8768 100644 --- a/grass_internal/src/serializer.rs +++ b/grass_internal/src/serializer.rs @@ -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 { - 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 { + 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 { + 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 { + 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 { + 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) -> 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 = 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(()) } diff --git a/grass_internal/src/value/arglist.rs b/grass_internal/src/value/arglist.rs index 4d0a069..de524b8 100644 --- a/grass_internal/src/value/arglist.rs +++ b/grass_internal/src/value/arglist.rs @@ -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 { diff --git a/grass_internal/src/value/mod.rs b/grass_internal/src/value/mod.rs index 742e878..13242cd 100644 --- a/grass_internal/src/value/mod.rs +++ b/grass_internal/src/value/mod.rs @@ -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> { - 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::>>>()? - .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::>>>()? - .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::>>>()? - .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 { + serialize_value( + self, + &Options::default().style(if is_compressed { + OutputStyle::Compressed + } else { + OutputStyle::Expanded + }), + span, + ) + } + + pub fn inspect(&self, span: Span) -> SassResult { + 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> { - 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::>>>()? - .join(sep.as_str()), - Brackets::Bracketed => format!( - "[{}]", - vals.iter() - .map(|x| x.inspect(span)) - .collect::>>>()? - .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::>>()? - .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::>>>()? - .join(", "), - )), - Value::ArgList(args) => Cow::Owned( - args.elems - .iter() - .filter(|x| !x.is_null()) - .map(|a| a.inspect(span)) - .collect::>>>()? - .join(", "), - ), - Value::True | Value::False | Value::Color(..) | Value::String(..) => { - self.to_css_string(span, false)? - } - }) - } - pub fn as_list(self) -> Vec { match self { Value::List(v, ..) => v, diff --git a/tests/imports.rs b/tests/imports.rs index a562750..1a5eedf 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -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), diff --git a/tests/inspect.rs b/tests/inspect.rs index 3388ce0..4c02dbd 100644 --- a/tests/inspect.rs +++ b/tests/inspect.rs @@ -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" diff --git a/tests/list.rs b/tests/list.rs index 2d5a023..8a4684d 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -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." +); diff --git a/tests/meta.rs b/tests/meta.rs index 7c2efc7..bf9a676 100644 --- a/tests/meta.rs +++ b/tests/meta.rs @@ -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