Skip to content

Commit 29af523

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 29af523

24 files changed

+306
-133
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ project adheres to
2323
### Improvements
2424

2525
* Basic support for `meta.load-css` mixin (PR #131).
26+
* Improved `calc` and `clamp` handling (PR #133).
2627
* Refactor source file handling. Instead of creating new FileContexts
2728
wrapping the original for each file for searching for local paths in
2829
that file, use the SourceName of the containing file to find local

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

+5-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,9 @@ 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+
// Either a nice semantic css function or a fallback with interpolation.
419+
alt((css_function, map(special_function_misc, Value::Literal)))(input)
418420
}
419421

420422
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)