use std::{cmp::Ordering, sync::Arc};
use codemap::{Span, Spanned};
use crate::{
color::Color,
common::{BinaryOp, Brackets, ListSeparator, QuoteKind},
error::SassResult,
evaluate::Visitor,
selector::Selector,
serializer::{inspect_value, serialize_value},
unit::Unit,
utils::is_special_function,
Options, OutputStyle,
};
pub(crate) use arglist::ArgList;
pub(crate) use calculation::*;
pub(crate) use map::SassMap;
pub(crate) use number::*;
pub(crate) use sass_function::{SassFunction, UserDefinedFunction};
pub(crate) use sass_number::{conversion_factor, SassNumber};
mod arglist;
mod calculation;
mod map;
mod number;
mod sass_function;
mod sass_number;
#[derive(Debug, Clone)]
pub(crate) enum Value {
True,
False,
Null,
Dimension(SassNumber),
List(Vec<Value>, ListSeparator, Brackets),
Color(Arc<Color>),
String(String, QuoteKind),
Map(SassMap),
ArgList(ArgList),
FunctionRef(Box<SassFunction>),
Calculation(SassCalculation),
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match self {
Value::Calculation(calc1) => match other {
Value::Calculation(calc2) => calc1 == calc2,
_ => false,
},
Value::String(s1, ..) => match other {
Value::String(s2, ..) => s1 == s2,
_ => false,
},
Value::Dimension(n1) => match other {
Value::Dimension(n2) => n1 == n2,
_ => false,
},
Value::List(list1, sep1, brackets1) => match other {
Value::List(list2, sep2, brackets2) => {
if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() {
false
} else {
for (a, b) in list1.iter().zip(list2) {
if a != b {
return false;
}
}
true
}
}
_ => false,
},
Value::Null => matches!(other, Value::Null),
Value::True => matches!(other, Value::True),
Value::False => matches!(other, Value::False),
Value::FunctionRef(fn1) => {
if let Value::FunctionRef(fn2) = other {
fn1 == fn2
} else {
false
}
}
Value::Map(map1) => {
if let Value::Map(map2) = other {
map1 == map2
} else {
false
}
}
Value::Color(color1) => {
if let Value::Color(color2) = other {
color1 == color2
} else {
false
}
}
Value::ArgList(list1) => match other {
Value::ArgList(list2) => list1 == list2,
Value::List(list2, ListSeparator::Comma, ..) => {
if list1.len() != list2.len() {
return false;
}
for (el1, el2) in list1.elems.iter().zip(list2) {
if el1 != el2 {
return false;
}
}
true
}
_ => false,
},
}
}
}
impl Eq for Value {}
impl Value {
pub fn with_slash(
self,
numerator: SassNumber,
denom: SassNumber,
span: Span,
) -> SassResult<Self> {
let mut number = self.assert_number(span)?;
number.as_slash = Some(Arc::new((numerator, denom)));
Ok(Value::Dimension(number))
}
pub fn assert_number(self, span: Span) -> SassResult<SassNumber> {
match self {
Value::Dimension(n) => Ok(n),
_ => Err((format!("{} is not a number.", self.inspect(span)?), span).into()),
}
}
pub fn assert_number_with_name(self, name: &str, span: Span) -> SassResult<SassNumber> {
match self {
Value::Dimension(n) => Ok(n),
_ => Err((
format!(
"${name}: {} is not a number.",
self.inspect(span)?,
name = name,
),
span,
)
.into()),
}
}
pub fn assert_color_with_name(self, name: &str, span: Span) -> SassResult<Arc<Color>> {
match self {
Value::Color(c) => Ok(c),
_ => Err((
format!(
"${name}: {} is not a color.",
self.inspect(span)?,
name = name,
),
span,
)
.into()),
}
}
pub fn assert_string_with_name(
self,
name: &str,
span: Span,
) -> SassResult<(String, QuoteKind)> {
match self {
Value::String(s, quotes) => Ok((s, quotes)),
_ => Err((
format!(
"${name}: {} is not a string.",
self.inspect(span)?,
name = name,
),
span,
)
.into()),
}
}
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().all(Value::is_blank),
Value::ArgList(v, ..) => v.is_blank(),
_ => false,
}
}
pub fn is_empty_list(&self) -> bool {
match self {
Value::List(v, ..) => v.is_empty(),
Value::Map(m) => m.is_empty(),
Value::ArgList(v) => v.elems.is_empty(),
_ => false,
}
}
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_truthy(&self) -> bool {
!matches!(self, Value::Null | Value::False)
}
pub fn unquote(self) -> Self {
match self {
Value::String(s1, _) => Value::String(s1, QuoteKind::None),
Value::List(v, sep, bracket) => {
Value::List(v.into_iter().map(Value::unquote).collect(), sep, bracket)
}
v => v,
}
}
pub const fn span(self, span: Span) -> Spanned<Self> {
Spanned { node: self, span }
}
pub fn kind(&self) -> &'static str {
match self {
Value::Color(..) => "color",
Value::String(..) => "string",
Value::Calculation(..) => "calculation",
Value::Dimension(..) => "number",
Value::List(..) => "list",
Value::FunctionRef(..) => "function",
Value::ArgList(..) => "arglist",
Value::True | Value::False => "bool",
Value::Null => "null",
Value::Map(..) => "map",
}
}
pub fn as_slash(&self) -> Option<Arc<(SassNumber, SassNumber)>> {
match self {
Value::Dimension(SassNumber { as_slash, .. }) => as_slash.clone(),
_ => None,
}
}
pub fn without_slash(self) -> Self {
match self {
Value::Dimension(SassNumber {
num,
unit,
as_slash: _,
}) => Value::Dimension(SassNumber {
num,
unit,
as_slash: None,
}),
_ => self,
}
}
pub fn is_special_function(&self) -> bool {
match self {
Value::String(s, QuoteKind::None) => is_special_function(s),
Value::Calculation(..) => true,
_ => false,
}
}
pub fn is_var(&self) -> bool {
match self {
Value::String(s, QuoteKind::None) => {
if s.len() < "var(--_)".len() {
return false;
}
s.starts_with("var(")
}
Value::Calculation(..) => true,
_ => false,
}
}
pub fn bool(b: bool) -> Self {
if b {
Value::True
} else {
Value::False
}
}
pub fn cmp(&self, other: &Self, span: Span, op: BinaryOp) -> SassResult<Option<Ordering>> {
Ok(match self {
Value::Dimension(SassNumber { num, unit, .. }) => match &other {
Value::Dimension(SassNumber {
num: num2,
unit: unit2,
..
}) => {
if !unit.comparable(unit2) {
return Err(
(format!("Incompatible units {} and {}.", unit2, unit), span).into(),
);
}
if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None {
num.partial_cmp(num2)
} else {
num.partial_cmp(&num2.convert(unit2, unit))
}
}
_ => {
return Err((
format!(
"Undefined operation \"{} {} {}\".",
self.inspect(span)?,
op,
other.inspect(span)?
),
span,
)
.into())
}
},
_ => {
return Err((
format!(
"Undefined operation \"{} {} {}\".",
self.inspect(span)?,
op,
other.inspect(span)?
),
span,
)
.into());
}
})
}
pub fn not_equals(&self, other: &Self) -> bool {
match self {
Value::String(s1, ..) => match other {
Value::String(s2, ..) => s1 != s2,
_ => true,
},
Value::Dimension(SassNumber {
num: n,
unit,
as_slash: _,
}) if !n.is_nan() => match other {
Value::Dimension(SassNumber {
num: n2,
unit: unit2,
as_slash: _,
}) if !n2.is_nan() => {
if !unit.comparable(unit2) {
true
} else if unit == unit2 {
n != n2
} else if unit == &Unit::None || unit2 == &Unit::None {
true
} else {
n != &n2.convert(unit2, unit)
}
}
_ => true,
},
Value::List(list1, sep1, brackets1) => match other {
Value::List(list2, sep2, brackets2) => {
if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() {
true
} else {
for (a, b) in list1.iter().zip(list2) {
if a.not_equals(b) {
return true;
}
}
false
}
}
_ => true,
},
s => s != other,
}
}
pub fn as_list(self) -> Vec<Value> {
match self {
Value::List(v, ..) => v,
Value::Map(m) => m.as_list(),
Value::ArgList(v) => v.elems,
v => vec![v],
}
}
pub fn separator(&self) -> ListSeparator {
match self {
Value::List(_, list_separator, _) => *list_separator,
Value::Map(..) | Value::ArgList(..) => ListSeparator::Comma,
_ => ListSeparator::Space,
}
}
pub fn to_selector(
self,
visitor: &mut Visitor,
name: &str,
allows_parent: bool,
span: Span,
) -> SassResult<Selector> {
let string = match self.clone().selector_string(span)? {
Some(v) => v,
None => return Err((format!("${}: {} is not a valid selector: it must be a string,\n a list of strings, or a list of lists of strings.", name, self.inspect(span)?), span).into()),
};
Ok(Selector(visitor.parse_selector_from_string(
&string,
allows_parent,
true,
span,
)?))
}
#[allow(clippy::only_used_in_recursion)]
fn selector_string(self, span: Span) -> SassResult<Option<String>> {
Ok(Some(match self {
Value::String(text, ..) => text,
Value::List(list, sep, ..) if !list.is_empty() => {
let mut result = Vec::new();
match sep {
ListSeparator::Comma => {
for complex in list {
if let Value::String(text, ..) = complex {
result.push(text);
} else if let Value::List(
_,
ListSeparator::Space | ListSeparator::Undecided,
..,
) = complex
{
result.push(match complex.selector_string(span)? {
Some(v) => v,
None => return Ok(None),
});
} else {
return Ok(None);
}
}
}
ListSeparator::Slash => return Ok(None),
ListSeparator::Space | ListSeparator::Undecided => {
for compound in list {
if let Value::String(text, ..) = compound {
result.push(text);
} else {
return Ok(None);
}
}
}
}
result.join(sep.as_str())
}
_ => return Ok(None),
}))
}
pub fn unary_plus(self, visitor: &mut Visitor, span: Span) -> SassResult<Self> {
Ok(match self {
Self::Dimension(SassNumber { .. }) => self,
Self::Calculation(..) => {
return Err((
format!("Undefined operation \"+{}\".", self.inspect(span)?),
span,
)
.into())
}
_ => Self::String(
format!(
"+{}",
&self.to_css_string(span, visitor.options.is_compressed())?
),
QuoteKind::None,
),
})
}
pub fn unary_neg(self, visitor: &mut Visitor, span: Span) -> SassResult<Self> {
Ok(match self {
Self::Calculation(..) => {
return Err((
format!("Undefined operation \"-{}\".", self.inspect(span)?),
span,
)
.into())
}
Self::Dimension(SassNumber {
num,
unit,
as_slash,
}) => Self::Dimension(SassNumber {
num: -num,
unit,
as_slash,
}),
_ => Self::String(
format!(
"-{}",
&self.to_css_string(span, visitor.options.is_compressed())?
),
QuoteKind::None,
),
})
}
pub fn unary_div(self, visitor: &mut Visitor, span: Span) -> SassResult<Self> {
Ok(Self::String(
format!(
"/{}",
&self.to_css_string(span, visitor.options.is_compressed())?
),
QuoteKind::None,
))
}
pub fn unary_not(self) -> Self {
match self {
Self::False | Self::Null => Self::True,
_ => Self::False,
}
}
}