Document first-class calc() (#575)

See sass/sass#818

Co-authored-by: Awjin Ahn <awjin@google.com>
This commit is contained in:
Natalie Weizenbaum 2021-09-13 23:16:42 +00:00 committed by GitHub
parent e55b07cdd4
commit d0e401e3f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 282 additions and 104 deletions

View File

@ -210,6 +210,14 @@ if (window.location.hash) {
"#configuring-modules": "/documentation/at-rules/use#configuration"
};
var redirect = redirects[window.location.hash];
if (redirect) window.location.href = redirect;
} else if (window.location.pathname == "/documentation/syntax/special-functions") {
var redirects = {
"#calc-clamp-element-progid-and-expression": "/documentation/syntax/special-functions#element-progid-and-expression",
"#min-and-max": "/documentation/values/calculations#min-and-max"
};
var redirect = redirects[window.location.hash];
if (redirect) window.location.href = redirect;
}

View File

@ -7,7 +7,7 @@ title: sass:meta
## Mixins
<% function 'meta.load-css($url, $with: null)' do %>
<% impl_status dart: '(unreleased)', libsass: false, ruby: false do %>
<% impl_status dart: '1.23.0', libsass: false, ruby: false do %>
Only Dart Sass currently supports this mixin.
<% end %>
@ -97,6 +97,43 @@ title: sass:meta
## Functions
<% function 'meta.calc-args($calc)', returns: 'list' do %>
<% impl_status dart: '1.40.0', libsass: false, ruby: false %>
Returns the arguments for the given [calculation].
[calculation]: ../values/calculations
If an argument is a number or a nested calculation, it's returned as that
type. Otherwise, it's returned as an unquoted string.
<% example(autogen_css: false) do %>
@debug meta.calc-args(calc(100px + 10%)); // unquote("100px + 10%")
@debug meta.calc-args(clamp(50px, var(--width), 1000px)); // 50px, unquote("var(--width)"), 1000px
===
@debug meta.calc-args(calc(100px + 10%)) // unquote("100px + 10%")
@debug meta.calc-args(clamp(50px, var(--width), 1000px)) // 50px, unquote("var(--width)"), 1000px
<% end %>
<% end %>
<% function 'meta.calc-name($calc)', returns: 'quoted string' do %>
<% impl_status dart: '1.40.0', libsass: false, ruby: false %>
Returns the name of the given [calculation].
[calculation]: ../values/calculations
<% example(autogen_css: false) do %>
@debug meta.calc-name(calc(100px + 10%)); // "calc"
@debug meta.calc-name(clamp(50px, var(--width), 1000px)); // "clamp"
===
@debug meta.calc-name(calc(100px + 10%)) // "calc"
@debug meta.calc-name(clamp(50px, var(--width), 1000px)) // "clamp"
<% end %>
<% end %>
<% function 'meta.call($function, $args...)', 'call($function, $args...)' do %>
<%= partial 'documentation/snippets/call-impl-status' %>
@ -476,6 +513,7 @@ title: sass:meta
* [`color`](../values/colors)
* [`list`](../values/lists)
* [`map`](../values/maps)
* [`calculation`](../values/calculations)
* [`bool`](../values/booleans)
* [`null`](../values/null)
* [`function`](../values/functions)

View File

@ -28,6 +28,8 @@ different types:
equal to space-separated lists, and bracketed lists aren't equal to
unbracketed lists.
* [Maps][] are equal if their keys and values are both equal.
* [Calculations] are equal if their names and arguments are all equal.
Operation arguments are compared textually.
* [`true`, `false`][], and [`null`][] are only equal to themselves.
* [Functions][] are equal to the same function. Functions are compared *by
reference*, so even if two functions have the same name and definition they're
@ -42,6 +44,7 @@ different types:
[`true`, `false`]: ../values/booleans
[`null`]: ../values/null
[Maps]: ../values/maps
[Calculations]: ../values/calculations
[Functions]: ../values/functions
<% example(autogen_css: false) do %>
@ -92,6 +95,9 @@ different types:
@debug $theme == ("venus": #998099, "nebula": #d2e1dd) // true
@debug $theme != ("venus": #998099, "iron": #dadbdf) // true
@debug calc(10px + 10%) == calc(10px + 10%) // true
@debug calc(10% + 10px) == calc(10px + 10%) // false
@debug true == true // true
@debug true != false // true
@debug null != false // true

View File

@ -84,22 +84,34 @@ calls][]—it's parsed as a normal [plain CSS function call][].
font-weight: 400
<% end %>
## `calc()`, `clamp()`, `element()`, `progid:...()`, and `expression()`
## `element()`, `progid:...()`, and `expression()`
<% impl_status dart: "1.31.0", libsass: false, ruby: false, feature: "clamp()" do %>
LibSass, Ruby Sass, and older versions of Dart Sass treat `clamp()` as a
[plain CSS function] rather than supporting special syntax within it.
<% impl_status dart: "<1.40.0", libsass: false, ruby: false, feature: "calc()" do %>
LibSass, Ruby Sass, and versions of Dart Sass prior to 1.40.0 parse `calc()`
as special syntactic function like `element()`.
[plain CSS function]: ../at-rules/function#plain-css-functions
Dart Sass versions 1.40.0 and later parse `calc()` as a [calculation].
[calculation]: ../values/calculations
<% end %>
The [`calc()`], [`clamp()`] and [`element()`] functions are defined in the CSS
spec. Because `calc()`'s mathematical expressions conflict with Sass's
arithmetic, and `element()`'s IDs could be parsed as colors, they need special
parsing.
<% impl_status dart: ">=1.31.0 <1.40.0", libsass: false, ruby: false, feature: "clamp()" do %>
LibSass, Ruby Sass, and versions of Dart Sass prior to 1.31.0 parse `clamp()`
as a [plain CSS function] rather than supporting special syntax within it.
[plain CSS function]: ../at-rules/function#plain-css-functions
Dart Sass versions between 1.31.0 and 1.40.0 parse `clamp()` as special
syntactic function like `element()`.
Dart Sass versions 1.40.0 and later parse `clamp()` as a [calculation].
[calculation]: ../values/calculations
<% end %>
The [`element()`] function is defined in the CSS spec, and because its IDs could
be parsed as colors, they need special parsing.
[`calc()`]: https://developer.mozilla.org/en-US/docs/Web/CSS/calc
[`clamp()`]: https://developer.mozilla.org/en-US/docs/Web/CSS/clamp
[`element()`]: https://developer.mozilla.org/en-US/docs/Web/CSS/element
[`expression()`][] and functions beginning with [`progid:`][] are legacy
@ -115,105 +127,18 @@ Nothing is interpreted as a SassScript expression, with the exception that
[interpolation][] can be used to inject dynamic values.
<% example do %>
@use "sass:math";
$logo-element: logo-bg;
.logo {
$width: 800px;
width: $width;
position: absolute;
left: calc(50% - #{math.div($width, 2)});
top: 0;
background: element(##{$logo-element});
}
===
@use "sass:math"
$logo-element: logo-bg
.logo
$width: 800px
width: $width
position: absolute
left: calc(50% - #{math.div($width, 2)})
top: 0
background: element(##{$logo-element})
===
.logo {
width: 800px;
position: absolute;
left: calc(50% - 400px);
top: 0;
}
<% end %>
## `min()` and `max()`
<% impl_status dart: "1.11.0", libsass: false, ruby: false do %>
LibSass and Ruby Sass currently *always* parse `min()` and `max()` as Sass
functions. To create a plain CSS `min()` or `max()` call for those
implementations, you can write something like `unquote("min(#{$padding},
env(safe-area-inset-left))")` instead.
<% end %>
CSS added support for [`min()` and `max()` functions][] in Values and Units
Level 4, from where they were quickly adopted by Safari [to support the
iPhoneX][]. But Sass supported its own [`min()`][] and [`max()`][] functions
long before this, and it needed to be backwards-compatible with all those
existing stylesheets. This led for the need for extra-special syntactic
cleverness.
[`min()` and `max()` functions]: https://drafts.csswg.org/css-values-4/#calc-notation
[to support the iPhoneX]: https://webkit.org/blog/7929/designing-websites-for-iphone-x/
[`min()`]: ../modules/math#min
[`max()`]: ../modules/math#max
If a `min()` or `max()` function call is valid plain CSS, it will be compiled to
a CSS `min()` or `max()` call. "Plain CSS" includes nested calls to
[`calc()`][], [`env()`][], [`var()`][], `min()`, or `max()`, as well as
[interpolation][]. As soon as any part of the call contains a SassScript feature
like [variables][] or [function calls][], though, it's parsed as a call to
Sass's core `min()` or `max()` function instead.
[`env()`]: https://developer.mozilla.org/en-US/docs/Web/CSS/env
[`var()`]: https://developer.mozilla.org/en-US/docs/Web/CSS/var
<!-- TODO(nweiz): auto-generate this CSS once we're compiling with Dart Sass -->
<% example do %>
$padding: 12px;
.post {
// Since these max() calls don't use any Sass features other than
// interpolation, they're compiled to CSS max() calls.
padding-left: max(#{$padding}, env(safe-area-inset-left));
padding-right: max(#{$padding}, env(safe-area-inset-right));
}
.sidebar {
// Since these refer to a Sass variable without interpolation, they call
// Sass's built-in max() function.
padding-left: max($padding, 20px);
padding-right: max($padding, 20px);
}
===
$padding: 12px
.post
// Since these max() calls don't use any Sass features other than
// interpolation, they're compiled to CSS max() calls.
padding-left: max(#{$padding}, env(safe-area-inset-left))
padding-right: max(#{$padding}, env(safe-area-inset-right))
.sidebar
// Since these refer to a Sass variable without interpolation, they call
// Sass's built-in max() function.
padding-left: max($padding, 20px)
padding-right: max($padding, 20px)
===
.post {
padding-left: max(12px, env(safe-area-inset-left));
padding-right: max(12px, env(safe-area-inset-right));
}
.sidebar {
padding-left: 20px;
padding-right: 20px;
background: element(#logo-bg);
}
<% end %>

View File

@ -0,0 +1,201 @@
---
title: Calculations
introduction: >
Calculations are how Sass represents the `calc()` function, as well as similar
functions like `clamp()`, `min()`, and `max()`. Sass will simplify these as
much as possible, even if they're combined with one another.
---
<% impl_status dart: '1.40.0', libsass: false, ruby: false do %>
LibSass, Ruby Sass, and versions of Dart Sass prior to 1.40.0 parse `calc()`
as a [special function] like `element()`.
[special function]: ../syntax/special-functions#element-progid-and-expression
LibSass, Ruby Sass, and versions of Dart Sass prior to 1.31.0 parse `clamp()`
as a [plain CSS function] rather than supporting special syntax within it.
Versions of Dart Sass between 1.31.0 and 1.40.0 parse `clamp()` as a [special
function] like `element()`.
[plain CSS function]: ../at-rules/function#plain-css-functions
<% end %>
<% example(autogen_css: false) do %>
@debug calc(400px + 10%); // calc(400px + 10%)
@debug calc(400px / 2); // 200px
@debug min(100px, calc(1rem + 10%)); // min(100px, 1rem + 10%)
===
@debug calc(400px + 10%) // calc(400px + 10%)
@debug calc(400px / 2) // 200px
@debug min(100px, calc(1rem + 10%) ; // min(100px, 1rem + 10%)
<% end %>
Calculations use a special syntax that's different from normal SassScript. It's
the same syntax as the CSS `calc()`, but with the additional ability to use
[Sass variables] and call [Sass functions]. This means that `/` is always a
division operator within a calculation!
[Sass variables]: ../variables
[Sass functions]: ../modules
<% fun_fact do %>
The arguments to a Sass function call use the normal Sass syntax, rather than
the special calculation syntax!
<% end %>
You can also use [interpolation] in a calculation. However, if you do, nothing
in the parentheses that surround that interpolation will be simplified or
type-checked, so it's easy to end up with extra verbose or even invalid CSS.
Rather than writing `calc(10px + #{$var})`, just write `calc(10px + $var)`!
[interpolation]: ../interpolation
## Simplification
Sass will simplify adjacent operations in calculations if they use units that
can be combined at compile-time, such as `1in + 10px` or `5s * 2`. If possible,
it'll even simplify the whole calculation to a single number—for example,
`clamp(0px, 30px, 20px)` will return `20px`.
<% heads_up do %>
This means that a calculation expression won't necessarily always return a
calculation! If you're writing a Sass library, you can always use the
[`meta.type-of()`] function to determine what type you're dealing with.
[`meta.type-of()`]: ../modules/meta#type-of
<% end %>
Calculations will also be simplified within other calculations. In particular,
if a `calc()` end up inside any other calculation, the function call will be
removed and it'll be replaced by a plain old operation.
<% example do %>
$width: calc(400px + 10%);
.sidebar {
width: $width;
padding-left: calc($width / 4);
}
===
$width: calc(400px + 10%)
.sidebar
width: $width
padding-left: calc($width / 4)
==
.sidebar {
width: calc(400px + 10%);
padding-left: calc((400px + 10%) / 4);
}
<% end %>
## Operations
You can't use calculations with normal SassScript operations like `+` and `*`.
If you want to write some math functions that allow calculations just write them
within their own `calc()` expressions—if they're passed a bunch of numbers with
compatible units, they'll return plain numbers as well, and if they're passed
calculations they'll return calculations.
This restriction is in place to make sure that if calculations *aren't* wanted,
they throw an error as soon as possible. Calculations can't be used everywhere
plain numbers can: they can't be injected into CSS identifiers (such as
`.item-#{$n}`), for example, and they can't be passed to Sass's built-in [math
functions]. Reserving SassScript operations for plain numbers makes it clear
exactly where calculations are allowed and where they aren't.
[math functions]: ../modules/math
<% example(autogen_css: false) do %>
$width: calc(100% + 10px);
@debug $width * 2; // Error!
@debug calc($width * 2); // calc((100% + 10px) * 2);
===
$width: calc(100% + 10px);
@debug $width * 2; // Error!
@debug calc($width * 2); // calc((100% + 10px) * 2);
<% end %>
## `min()` and `max()`
<% impl_status dart: ">=1.11.0 <1.40.0", libsass: false, ruby: false, feature: "Special function syntax" do %>
LibSass, Ruby Sass, and versions of Dart Sass prior to 1.11.0 *always* parse
`min()` and `max()` as Sass functions. To create a plain CSS `min()` or
`max()` call for those implementations, you can write something like
`unquote("min(#{$padding}, env(safe-area-inset-left))")` instead.
Versions of Dart Sass between 1.11.0 and 1.40.0 parse `min()` and `max()`
functions as [special functions] if they're valid plain CSS, but parses them
as Sass functions if they contain Sass features other than interpolation, like
variables or function calls.
[special functions]: ../syntax/special-functions
<% end %>
CSS added support for [`min()` and `max()` functions] in Values and Units Level
4, from where they were quickly adopted by Safari [to support the iPhoneX]. But
Sass supported its own [`min()`] and [`max()`] functions long before this, and
it needed to be backwards-compatible with all those existing stylesheets. This
led to the need for extra-special syntactic cleverness.
[`min()` and `max()` functions]: https://drafts.csswg.org/css-values-4/#calc-notation
[to support the iPhoneX]: https://webkit.org/blog/7929/designing-websites-for-iphone-x/
[`min()`]: ../modules/math#min
[`max()`]: ../modules/math#max
If a `min()` or `max()` function call is a valid calculation expression, it will
be parsed as a calculation. But as soon as any part of the call contains a
SassScript feature that isn't supported in a calculation, like the [modulo
operator], it's parsed as a call to Sass's core `min()` or `max()` function
instead.
Since calculations are simplified to numbers when possible anyway, the only
substantive difference is that the Sass functions only support units that can be
combined at build time, so `min(12px % 10, 10%)` will throw an error.
[modulo operator]: ../operators/numeric
<!-- TODO(nweiz): auto-generate this CSS once we're compiling with Dart Sass -->
<% example do %>
$padding: 12px;
.post {
// Since these max() calls are valid calculation expressions, they're
// parsed as calculations.
padding-left: max($padding, env(safe-area-inset-left));
padding-right: max($padding, env(safe-area-inset-right));
}
.sidebar {
// Since these use the SassScript-only modulo operator, they're parsed as
// SassScript function calls.
padding-left: max($padding % 10, 20px);
padding-right: max($padding % 10, 20px);
}
===
$padding: 12px
.post
// Since these max() calls are valid calculation expressions, they're
// parsed as calculations.
padding-left: max($padding, env(safe-area-inset-left))
padding-right: max($padding, env(safe-area-inset-right))
.sidebar
// Since these use the SassScript-only modulo operator, they're parsed as
// SassScript function calls.
padding-left: max($padding % 10, 20px)
padding-right: max($padding % 10, 20px)
===
.post {
padding-left: max(12px, env(safe-area-inset-left));
padding-right: max(12px, env(safe-area-inset-right));
}
.sidebar {
padding-left: 20px;
padding-right: 20px;
}
<% end %>