unify serialization and inspection
This commit is contained in:
parent
d8867e42db
commit
d14e6bd9f7
@ -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
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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, ..) => {
|
||||
|
@ -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)]
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
),
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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> {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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"
|
||||
|
@ -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."
|
||||
);
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user