Skip to content

Commit 077370b

Browse files
committed
Improve the calc and clamp functions.
The `calc` and `clamp` functions are very special, since the sass function extrends but does not replace the css function. So depending on the arguments, it may be either or even kind-of both.
1 parent 8b8099a commit 077370b

23 files changed

+306
-133
lines changed

Diff for: src/css/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ pub use self::selectors::{BadSelector, Selector, SelectorPart, Selectors};
1313
pub use self::string::CssString;
1414
pub use self::value::{Value, ValueMap, ValueToMapError};
1515

16-
pub(crate) use self::util::is_not;
16+
pub(crate) use self::util::{is_function_name, is_not};

Diff for: src/css/string.rs

+14
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ impl CssString {
120120
pub fn is_null(&self) -> bool {
121121
self.value.is_empty() && self.quotes.is_none()
122122
}
123+
/// Return true if this is a css special function call.
124+
pub(crate) fn is_css_fn(&self) -> bool {
125+
let value = self.value();
126+
self.quotes() == Quotes::None
127+
&& value.ends_with(')')
128+
&& (value.starts_with("calc(") || value.starts_with("var("))
129+
}
130+
/// Return true if this is a css special function call.
131+
pub(crate) fn is_css_calc(&self) -> bool {
132+
let value = self.value();
133+
self.quotes() == Quotes::None
134+
&& value.ends_with(')')
135+
&& (value.starts_with("calc(") || value.starts_with("clamp("))
136+
}
123137
/// Access the string value
124138
pub fn value(&self) -> &str {
125139
&self.value

Diff for: src/css/util.rs

+5
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ where
1313
expected,
1414
)
1515
}
16+
17+
/// Return true iff s is a valid _css_ function name.
18+
pub fn is_function_name(s: &str) -> bool {
19+
s == "calc" || s == "clamp" || s == "max" || s == "min" || s == "var"
20+
}

Diff for: src/css/value.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{is_not, CallArgs, CssString};
1+
use super::{is_function_name, is_not, CallArgs, CssString};
22
use crate::error::Error;
33
use crate::ordermap::OrderMap;
44
use crate::output::{Format, Formatted};
@@ -62,8 +62,12 @@ impl Value {
6262
pub fn type_name(&self) -> &'static str {
6363
match *self {
6464
Value::ArgList(..) => "arglist",
65-
Value::Call(..) => "calculation",
65+
Value::Call(ref name, _) if is_function_name(name) => {
66+
"calculation"
67+
}
68+
Value::Call(..) => "string",
6669
Value::Color(..) => "color",
70+
Value::Literal(ref s) if s.is_css_calc() => "calculation",
6771
Value::Literal(..) => "string",
6872
Value::Map(..) => "map",
6973
Value::Numeric(..) => "number",

Diff for: src/css/valueformat.rs

+49-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::Value;
1+
use super::{is_function_name, Value};
22
use crate::output::Formatted;
33
use crate::value::{ListSeparator, Operator};
44
use std::fmt::{self, Display, Write};
@@ -76,13 +76,47 @@ impl<'a> Display for Formatted<'a, Value> {
7676
write!(out, "{}({})", name, arg)
7777
}
7878
Value::BinOp(ref a, _, Operator::Plus, _, ref b)
79-
if a.type_name() != "number" || b.type_name() != "number" =>
79+
if add_as_join(a) || add_as_join(b) =>
8080
{
8181
// The plus operator is also a concat operator
8282
a.format(self.format).fmt(out)?;
8383
b.format(self.format).fmt(out)
8484
}
8585
Value::BinOp(ref a, ref s1, ref op, ref s2, ref b) => {
86+
use Operator::{Minus, Plus};
87+
let (op, b) = match (op, b.as_ref()) {
88+
(Plus, Value::Numeric(v, _)) if v.value.is_negative() => {
89+
(Minus, Value::from(-v))
90+
}
91+
(Minus, Value::Numeric(v, _))
92+
if v.value.is_negative() =>
93+
{
94+
(Plus, Value::from(-v))
95+
}
96+
(op, Value::Paren(p)) => {
97+
if let Some(op2) = is_op(p.as_ref()) {
98+
if op2 > *op {
99+
(op.clone(), *p.clone())
100+
} else {
101+
(op.clone(), *b.clone())
102+
}
103+
} else {
104+
(op.clone(), *b.clone())
105+
}
106+
}
107+
(op, Value::BinOp(_, _, op2, _, _))
108+
if (op2 < op) || (*op == Minus && *op2 == Minus) =>
109+
{
110+
(op.clone(), Value::Paren(b.clone()))
111+
}
112+
(op, v) => (op.clone(), v.clone()),
113+
};
114+
fn is_op(v: &Value) -> Option<Operator> {
115+
match v {
116+
Value::BinOp(_, _, op, _, _) => Some(op.clone()),
117+
_ => None,
118+
}
119+
}
86120
a.format(self.format).fmt(out)?;
87121
if *s1 {
88122
out.write_char(' ')?;
@@ -161,3 +195,16 @@ impl<'a> Display for Formatted<'a, Value> {
161195
}
162196
}
163197
}
198+
199+
fn add_as_join(v: &Value) -> bool {
200+
match v {
201+
Value::List(..) => true,
202+
Value::Literal(ref s) => !s.is_css_fn(),
203+
Value::Call(ref name, _) => !is_function_name(name),
204+
Value::BinOp(ref a, _, Operator::Plus, _, ref b) => {
205+
add_as_join(a) || add_as_join(b)
206+
}
207+
Value::True | Value::False => true,
208+
_ => false,
209+
}
210+
}

Diff for: src/parser/css_function.rs

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//! The `calc` function is special. A css function that is partially evaluated in sass.
2+
//! This should apply to `min`, `max` and `clamp` as well.
3+
use super::util::{opt_spacelike, spacelike2};
4+
use super::value::{function_call, number, special_function, variable};
5+
use super::{ignore_comments, PResult, SourcePos, Span};
6+
use crate::sass::{CallArgs, Value};
7+
use crate::value::Operator;
8+
use nom::branch::alt;
9+
use nom::bytes::complete::{tag, tag_no_case};
10+
use nom::character::complete::multispace0;
11+
use nom::combinator::{map, not, peek, value};
12+
use nom::sequence::{delimited, preceded, terminated, tuple};
13+
14+
pub fn css_function(input: Span) -> PResult<Value> {
15+
let (rest, arg) = delimited(
16+
terminated(tag_no_case("calc("), ignore_comments),
17+
sum_expression,
18+
preceded(ignore_comments, tag(")")),
19+
)(input)?;
20+
let pos = SourcePos::from_to(input, rest);
21+
Ok((
22+
rest,
23+
Value::Call("calc".into(), CallArgs::new_single(arg), pos),
24+
))
25+
}
26+
27+
fn sum_expression(input: Span) -> PResult<Value> {
28+
let (mut rest, mut v) = term(input)?;
29+
while let Ok((nrest, (s1, op, s2, v2))) = alt((
30+
tuple((
31+
value(false, tag("")),
32+
alt((
33+
value(Operator::Plus, tag("+")),
34+
value(Operator::Minus, tag("-")),
35+
)),
36+
map(multispace0, |s: Span| !s.fragment().is_empty()),
37+
term,
38+
)),
39+
tuple((
40+
value(true, spacelike2),
41+
alt((
42+
value(Operator::Plus, tag("+")),
43+
value(Operator::Minus, terminated(tag("-"), spacelike2)),
44+
)),
45+
alt((value(true, spacelike2), value(false, tag("")))),
46+
term,
47+
)),
48+
))(rest)
49+
{
50+
v = Value::BinOp(Box::new(v), s1, op, s2, Box::new(v2));
51+
rest = nrest;
52+
}
53+
Ok((rest, v))
54+
}
55+
56+
fn term(input: Span) -> PResult<Value> {
57+
let (mut rest, mut v) = single_value(input)?;
58+
while let Ok((nrest, (s1, op, s2, v2))) = tuple((
59+
map(multispace0, |s: Span| !s.fragment().is_empty()),
60+
alt((
61+
value(Operator::Multiply, tag("*")),
62+
value(Operator::Div, terminated(tag("/"), peek(not(tag("/"))))),
63+
value(Operator::Modulo, tag("%")),
64+
)),
65+
map(multispace0, |s: Span| !s.fragment().is_empty()),
66+
single_value,
67+
))(rest)
68+
{
69+
rest = nrest;
70+
v = Value::BinOp(Box::new(v), s1, op, s2, Box::new(v2));
71+
}
72+
Ok((rest, v))
73+
}
74+
75+
fn single_value(input: Span) -> PResult<Value> {
76+
alt((
77+
paren,
78+
value(Value::True, tag("true")),
79+
value(Value::False, tag("false")),
80+
value(Value::HereSelector, tag("&")),
81+
number,
82+
variable,
83+
value(Value::Null, tag("null")),
84+
special_function,
85+
function_call,
86+
))(input)
87+
}
88+
89+
fn paren(input: Span) -> PResult<Value> {
90+
map(
91+
delimited(
92+
terminated(tag("("), opt_spacelike),
93+
sum_expression,
94+
preceded(opt_spacelike, tag(")")),
95+
),
96+
|inner| Value::Paren(Box::new(inner), false),
97+
)(input)
98+
}

Diff for: src/parser/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ macro_rules! check_parse {
88
}
99

1010
pub(crate) mod css;
11+
mod css_function;
1112
mod error;
1213
pub mod formalargs;
1314
mod imports;

Diff for: src/parser/value.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::css_function::css_function;
12
use super::formalargs::call_args;
23
use super::strings::{
34
name, sass_string_dq, sass_string_ext, sass_string_sq,
@@ -304,7 +305,7 @@ fn sign_prefix(input: Span) -> PResult<Option<&[u8]>> {
304305
.map(|(r, s)| (r, s.map(|s| *s.fragment())))
305306
}
306307

307-
fn number(input: Span) -> PResult<Value> {
308+
pub fn number(input: Span) -> PResult<Value> {
308309
map(
309310
tuple((
310311
sign_prefix,
@@ -413,8 +414,10 @@ pub fn unary_op(input: Span) -> PResult<Value> {
413414
)(input)
414415
}
415416

416-
fn special_function(input: Span) -> PResult<Value> {
417-
map(special_function_misc, Value::Literal)(input)
417+
pub fn special_function(input: Span) -> PResult<Value> {
418+
// Recognize a single argument (that may be a list?)
419+
// But we do _not_ want to recognize anything containing an interpolation.
420+
alt((css_function, map(special_function_misc, Value::Literal)))(input)
418421
}
419422

420423
pub fn function_call(input: Span) -> PResult<Value> {

Diff for: src/sass/call_args.rs

+8
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ impl CallArgs {
3838
Ok(CallArgs { positional, named })
3939
}
4040

41+
/// Create a new CallArgs from one single unnamed argument.
42+
pub fn new_single(value: Value) -> Self {
43+
CallArgs {
44+
positional: vec![value],
45+
named: Default::default(),
46+
}
47+
}
48+
4149
/// Evaluate these sass CallArgs to css CallArgs.
4250
pub fn evaluate(&self, scope: ScopeRef) -> Result<css::CallArgs, Error> {
4351
let positional = Vec::new();

Diff for: src/sass/functions/math.rs

+25-24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::css::{CallArgs, CssString, Value};
66
use crate::output::Format;
77
use crate::sass::Name;
88
use crate::value::{Number, Numeric, Quotes, Rational, Unit, UnitSet};
9+
use crate::ScopeRef;
910
use std::cmp::Ordering;
1011
use std::f64::consts::{E, PI};
1112

@@ -47,30 +48,7 @@ pub fn create_module() -> Scope {
4748
let val = get_numeric(s, "number")?;
4849
Ok(number(val.value.ceil(), val.unit))
4950
});
50-
def!(f, clamp(min, number, max), |s| {
51-
let min_v = get_numeric(s, "min")?;
52-
let check_numeric_compat_unit =
53-
|v: Value| -> Result<Numeric, String> {
54-
let v = check::numeric(v)?;
55-
if (v.is_no_unit() != min_v.is_no_unit())
56-
|| !v.unit.is_compatible(&min_v.unit)
57-
{
58-
return Err(diff_units_msg(&v, &min_v, name!(min)));
59-
}
60-
Ok(v)
61-
};
62-
let mut num =
63-
get_checked(s, name!(number), check_numeric_compat_unit)?;
64-
let max_v = get_checked(s, name!(max), check_numeric_compat_unit)?;
65-
66-
if num >= max_v {
67-
num = max_v;
68-
}
69-
if num <= min_v {
70-
num = min_v;
71-
}
72-
Ok(Value::Numeric(num, true))
73-
});
51+
def!(f, clamp(min, number, max), clamp_fn);
7452
def!(f, floor(number), |s| {
7553
let val = get_numeric(s, "number")?;
7654
Ok(number(val.value.floor(), val.unit))
@@ -371,3 +349,26 @@ fn diff_units_msg(
371349
}
372350
)
373351
}
352+
353+
pub(crate) fn clamp_fn(s: &ScopeRef) -> Result<Value, Error> {
354+
let min_v = get_numeric(s, "min")?;
355+
let check_numeric_compat_unit = |v: Value| -> Result<Numeric, String> {
356+
let v = check::numeric(v)?;
357+
if (v.is_no_unit() != min_v.is_no_unit())
358+
|| !v.unit.is_compatible(&min_v.unit)
359+
{
360+
return Err(diff_units_msg(&v, &min_v, name!(min)));
361+
}
362+
Ok(v)
363+
};
364+
let mut num = get_checked(s, name!(number), check_numeric_compat_unit)?;
365+
let max_v = get_checked(s, name!(max), check_numeric_compat_unit)?;
366+
367+
if num >= max_v {
368+
num = max_v;
369+
}
370+
if num <= min_v {
371+
num = min_v;
372+
}
373+
Ok(Value::Numeric(num, true))
374+
}

Diff for: src/sass/functions/meta.rs

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub fn create_module() -> Scope {
1515
// - - - Functions - - -
1616
def!(f, calc_args(calc), |s| {
1717
get_checked(s, name!(calc), |v| match v {
18+
Value::Call(name, args) if name == "calc" => {
19+
// TODO: Maybe allow a single numeric argument to be itself?
20+
Ok(args.to_string().into())
21+
}
1822
Value::Call(_, args) => Ok(args.into()),
1923
Value::Literal(s) if looks_like_call(&s) => {
2024
let s = s.value();

0 commit comments

Comments
 (0)