use crate::css::{self, is_not, CallArgs, CssString, Value};
use crate::error::Error;
use crate::output::{Format, Formatted};
use crate::parser::SourcePos;
use crate::sass::{FormalArgs, Name};
use crate::value::{CssDimension, Numeric, Operator, Quotes};
use crate::{sass, Scope, ScopeRef};
use lazy_static::lazy_static;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{cmp, fmt};
#[macro_use]
mod macros;
mod color;
mod list;
mod map;
mod math;
mod meta;
mod selector;
mod string;
type BuiltinFn = dyn Fn(&ScopeRef) -> Result<Value, Error> + Send + Sync;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
pub struct Function {
args: FormalArgs,
pos: SourcePos,
body: FuncImpl,
}
#[derive(Clone)]
pub enum FuncImpl {
Builtin(Arc<BuiltinFn>),
UserDefined(ScopeRef, Vec<sass::Item>),
}
impl PartialOrd for FuncImpl {
fn partial_cmp(&self, rhs: &Self) -> Option<cmp::Ordering> {
match (self, rhs) {
(&FuncImpl::Builtin(..), &FuncImpl::Builtin(..)) => None,
(&FuncImpl::Builtin(..), &FuncImpl::UserDefined(..)) => {
Some(cmp::Ordering::Less)
}
(&FuncImpl::UserDefined(..), &FuncImpl::Builtin(..)) => {
Some(cmp::Ordering::Greater)
}
(
&FuncImpl::UserDefined(ref _sa, ref a),
&FuncImpl::UserDefined(ref _sb, ref b),
) => a.partial_cmp(b),
}
}
}
impl cmp::PartialEq for FuncImpl {
fn eq(&self, rhs: &FuncImpl) -> bool {
match (self, rhs) {
(
&FuncImpl::UserDefined(ref sa, ref a),
&FuncImpl::UserDefined(ref sb, ref b),
) => ScopeRef::is_same(sa, sb) && a == b,
(&FuncImpl::Builtin(ref a), &FuncImpl::Builtin(ref b)) => {
#[allow(clippy::vtable_address_comparisons)]
Arc::ptr_eq(a, b)
}
_ => false,
}
}
}
impl cmp::Eq for FuncImpl {}
impl fmt::Debug for FuncImpl {
fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
FuncImpl::Builtin(_) => write!(out, "(builtin function)"),
FuncImpl::UserDefined(..) => {
write!(out, "(user-defined function)")
}
}
}
}
trait Functions {
fn builtin_fn(
&mut self,
name: Name,
args: FormalArgs,
body: Arc<BuiltinFn>,
);
}
impl Functions for Scope {
fn builtin_fn(
&mut self,
name: Name,
args: FormalArgs,
body: Arc<BuiltinFn>,
) {
let f = Function::builtin(&self.get_name(), &name, args, body);
self.define_function(name, f);
}
}
impl Function {
pub fn get_builtin(name: &Name) -> Option<&'static Function> {
FUNCTIONS.get(name)
}
pub fn builtin(
module: &str,
name: &Name,
args: FormalArgs,
body: Arc<BuiltinFn>,
) -> Self {
let pos = SourcePos::mock_function(name, &args, module);
Function {
args,
pos,
body: FuncImpl::Builtin(body),
}
}
pub fn closure(
args: FormalArgs,
pos: SourcePos,
scope: ScopeRef,
body: Vec<sass::Item>,
) -> Self {
Function {
args,
pos,
body: FuncImpl::UserDefined(scope, body),
}
}
pub fn call(
&self,
callscope: ScopeRef,
args: CallArgs,
) -> Result<Value, Error> {
let cs = "%%CALLING_SCOPE%%";
match self.body {
FuncImpl::Builtin(ref body) => {
let s = self.do_eval_args(
ScopeRef::new_global(callscope.get_format()),
args,
)?;
s.define_module(cs.into(), callscope);
body(&s)
}
FuncImpl::UserDefined(ref defscope, ref body) => {
let s = self.do_eval_args(defscope.clone(), args)?;
s.define_module(cs.into(), callscope);
Ok(s.eval_body(body)?.unwrap_or(Value::Null))
}
}
.map(Value::into_calculated)
}
fn do_eval_args(
&self,
def: ScopeRef,
args: CallArgs,
) -> Result<ScopeRef, Error> {
self.args.eval(def, args).map_err(|e| match e {
sass::ArgsError::Eval(e) => e,
ae => Error::BadArguments(ae.to_string(), self.pos.clone()),
})
}
}
lazy_static! {
static ref MODULES: BTreeMap<&'static str, Scope> = {
let mut modules = BTreeMap::new();
modules.insert("sass:color", color::create_module());
modules.insert("sass:list", list::create_module());
modules.insert("sass:map", map::create_module());
modules.insert("sass:math", math::create_module());
modules.insert("sass:meta", meta::create_module());
modules.insert("sass:selector", selector::create_module());
modules.insert("sass:string", string::create_module());
modules
};
}
pub fn get_global_module(name: &str) -> Option<ScopeRef> {
MODULES.get(name).map(ScopeRef::Builtin)
}
type FunctionMap = BTreeMap<Name, Function>;
impl Functions for FunctionMap {
fn builtin_fn(
&mut self,
name: Name,
args: FormalArgs,
body: Arc<BuiltinFn>,
) {
let f = Function::builtin("", &name, args, body);
self.insert(name, f);
}
}
lazy_static! {
static ref FUNCTIONS: FunctionMap = {
let mut f = BTreeMap::new();
def!(f, if(condition, if_true, if_false), |s| {
if s.get(&name!(condition))?.is_true() {
Ok(s.get(&name!(if_true))?)
} else {
Ok(s.get(&name!(if_false))?)
}
});
def!(f, calc(expr), |s| {
fn pre_calc(v: &Value) -> bool {
match v {
Value::Numeric(..) => true,
Value::Call(ref name, _) => css::is_function_name(name),
Value::Literal(s) => s.is_css_calc(),
_ => false,
}
}
fn css_dim(v: &Value) -> Option<Vec<(CssDimension, i8)>> {
match v {
Value::Numeric(num, _) => {
let u = &num.unit;
if u.is_known() && !u.is_percent() {
Some(u.css_dimension())
} else {
None
}
}
_ => None,
}
}
fn in_calc(v: Value) -> Result<Value, Error> {
match v {
Value::Literal(s) if s.quotes() == Quotes::None => {
if let Some(arg) = s
.value()
.strip_prefix("calc(")
.and_then(|s| s.strip_suffix(')'))
{
Ok(Value::Paren(Box::new(arg.into())))
} else {
Ok(s.into())
}
}
Value::Call(name, args) => {
if name == "calc"
&& args.check_no_named().is_ok()
&& args.positional.len() == 1
{
Ok(Value::Paren(Box::new(
args.positional.into_iter().next().unwrap(),
)))
} else {
Ok(Value::Call(name, args))
}
}
Value::BinOp(a, _, op, _, b) => {
let a = in_calc(*a)?;
let b = in_calc(*b)?;
if let (Some(adim), Some(bdim)) = (css_dim(&a), css_dim(&b)) {
if (op == Operator::Plus || op == Operator::Minus) && adim != bdim {
return Err(Error::error(format!(
"{} and {} are incompatible.",
a.format(Format::introspect()),
b.format(Format::introspect()),
)))
}
}
Ok(Value::BinOp(
Box::new(a),
true,
op,
true,
Box::new(b),
))
}
Value::Numeric(num, c) => {
if num.unit.valid_in_css() {
Ok(Value::Numeric(num, c))
} else {
Err(Error::error(format!(
"Number {} isn't compatible with CSS calculations.",
num.format(Format::introspect())
)))
}
}
v @ Value::Paren(..) => Ok(v),
v => Err(Error::error(format!(
"Value {} can't be used in a calculation.",
v.format(Format::introspect())
))),
}
}
let v = s.get(&name!(expr))?;
if pre_calc(&v) {
Ok(v)
} else {
Ok(Value::Call(
"calc".into(),
CallArgs::from_value(in_calc(v)?)?,
))
}
});
def!(f, clamp(min, number = b"null", max = b"null"), |s| {
self::math::clamp_fn(s).or_else(|_| {
let mut args = vec![s.get(&name!(min))?];
let b = s.get(&name!(number))?;
if !b.is_null() {
args.push(b);
}
let c = s.get(&name!(max))?;
if !c.is_null() {
args.push(c);
}
Ok(css::Value::Call(
"clamp".into(),
css::CallArgs::from_list(args),
))
})
});
color::expose(MODULES.get("sass:color").unwrap(), &mut f);
list::expose(MODULES.get("sass:list").unwrap(), &mut f);
map::expose(MODULES.get("sass:map").unwrap(), &mut f);
math::expose(MODULES.get("sass:math").unwrap(), &mut f);
meta::expose(MODULES.get("sass:meta").unwrap(), &mut f);
selector::expose(MODULES.get("sass:selector").unwrap(), &mut f);
string::expose(MODULES.get("sass:string").unwrap(), &mut f);
f
};
}
trait CheckedArg<T> {
fn named(self, name: Name) -> Result<T, Error>;
}
impl<T> CheckedArg<T> for Result<T, String> {
fn named(self, name: Name) -> Result<T, Error> {
self.map_err(|e| Error::BadArgument(name, e))
}
}
fn get_checked<T, F>(s: &Scope, name: Name, check: F) -> Result<T, Error>
where
F: Fn(Value) -> Result<T, String>,
{
check(s.get(&name)?).named(name)
}
fn get_opt_check<T, F>(
s: &Scope,
name: Name,
check: F,
) -> Result<Option<T>, Error>
where
F: Fn(Value) -> Result<T, String>,
{
match s.get(&name)? {
Value::Null => Ok(None),
v => check(v).named(name).map(Some),
}
}
fn get_numeric(s: &Scope, name: &str) -> Result<Numeric, Error> {
get_checked(s, name.into(), check::numeric)
}
fn get_integer(s: &Scope, name: Name) -> Result<i64, Error> {
get_checked(s, name, check::unitless_int)
}
pub fn get_string(s: &Scope, name: Name) -> Result<CssString, Error> {
get_checked(s, name, check::string)
}
fn get_va_list(s: &Scope, name: Name) -> Result<Vec<Value>, Error> {
get_checked(s, name, check::va_list)
}
fn expected_to<'a, T>(value: &'a T, cond: &str) -> String
where
Formatted<'a, T>: std::fmt::Display,
{
format!(
"Expected {} to {}.",
Formatted {
value,
format: Format::introspect()
},
cond,
)
}
fn is_special(v: &Value) -> bool {
match v {
Value::Call(..) => true,
Value::Literal(s) if looks_like_call(s) => true,
Value::BinOp(..) => true,
_ => false,
}
}
fn looks_like_call(s: &CssString) -> bool {
s.quotes().is_none()
&& s.value().contains('(')
&& s.value().ends_with(')')
}
mod check {
use super::{expected_to, is_not};
use crate::css::{CssString, Value};
use crate::value::{ListSeparator, Number, Numeric};
pub fn numeric(v: Value) -> Result<Numeric, String> {
v.numeric_value().map_err(|v| is_not(&v, "a number"))
}
pub fn int(v: Value) -> Result<i64, String> {
numeric(v)?
.value
.into_integer()
.map_err(|v| is_not(&v, "an int"))
}
pub fn unitless(v: Value) -> Result<Number, String> {
let val = numeric(v)?;
if val.is_no_unit() {
Ok(val.value)
} else {
Err(expected_to(&val, "have no units"))
}
}
pub fn unitless_int(v: Value) -> Result<i64, String> {
unitless(v)?
.into_integer()
.map_err(|v| is_not(&v, "an int"))
}
pub fn string(v: Value) -> Result<CssString, String> {
match v {
Value::Literal(s) => Ok(s),
Value::Call(name, args) => {
Ok(format!("{}({})", name, args).into())
}
v => Err(is_not(&v, "a string")),
}
}
pub fn va_list(v: Value) -> Result<Vec<Value>, String> {
match v {
Value::ArgList(args) => {
args.check_no_named().map_err(|e| e.to_string())?;
Ok(args.positional)
}
Value::List(v, Some(ListSeparator::Comma), _) => Ok(v),
single => Ok(vec![single]),
}
}
pub fn va_list_nonempty(v: Value) -> Result<Vec<Value>, String> {
let result = va_list(v)?;
if result.is_empty() {
Err("At least one selector must be passed.".into())
} else {
Ok(result)
}
}
}
#[test]
fn test_rgb() -> Result<(), Box<dyn std::error::Error>> {
use crate::parser::code_span;
use crate::parser::formalargs::call_args;
use crate::value::Rgba;
let scope = ScopeRef::new_global(Default::default());
assert_eq!(
FUNCTIONS.get(&name!(rgb)).unwrap().call(
scope.clone(),
call_args(code_span(b"(17, 0, 225)"))?.1.evaluate(scope)?
)?,
Value::Color(Rgba::from_rgb(17, 0, 225).into(), None)
);
Ok(())
}
#[test]
fn test_nth() {
assert_eq!("foo", do_evaluate(&[("x", "foo, bar")], b"nth($x, 1);"))
}
#[cfg(test)]
use crate::variablescope::test::do_evaluate;