gerillass 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:math";
5
+ @use "sass:meta";
6
+ @use "sass:string";
7
+
8
+ // True for an unquoted call to one of $names, such as var(--x). A quoted string
9
+ // stays text, so `"var(--x)"` is not a call.
10
+ @function -is-css-function($value, $names) {
11
+ @if meta.type-of($value) != "string" or string.slice(meta.inspect($value), 1, 1) == "\"" {
12
+ @return false;
13
+ }
14
+ $open: string.index($value, "(");
15
+ @return $open != null and list.index($names, string.to-lower-case(string.slice($value, 1, $open - 1))) != null;
16
+ }
17
+
18
+ // Truncates text after a number of lines. `ellipsis` does one line; this does
19
+ // several, and needs five declarations rather than one because -webkit-line-clamp
20
+ // only works alongside two other properties.
21
+ //
22
+ // All five were measured in a browser, on a paragraph that runs to five lines
23
+ // with a clamp of three:
24
+ //
25
+ // the three together 3 lines, with an ellipsis
26
+ // without -webkit-box-orient 5 lines, no clamping and no warning
27
+ // without display: -webkit-box 5 lines, likewise
28
+ // the standard line-clamp alone 5 lines; it is not Baseline yet
29
+ // without overflow: hidden the box is 3 lines, but the rest of the text
30
+ // spills out below it
31
+ //
32
+ // So the standard property is emitted for the future and the prefixed trio is
33
+ // what actually does the work today.
34
+ @mixin line-clamp($lines: 3) {
35
+ // var() is resolved by the browser, so only a literal count is checked.
36
+ @if $lines != none and not -is-css-function($lines, "var" "env") {
37
+ @if meta.type-of($lines) != "number" or not math.is-unitless($lines) {
38
+ @error "`#{$lines}` is not a valid $lines for `line-clamp`. Pass a whole number of lines, or `none` to undo a clamp.";
39
+ }
40
+ @if $lines < 1 or $lines != math.round($lines) {
41
+ @error "`#{$lines}` is not a valid $lines for `line-clamp`. Pass a whole number of lines that is at least 1, or `none` to undo a clamp.";
42
+ }
43
+ }
44
+ display: -webkit-box;
45
+ -webkit-box-orient: vertical;
46
+ -webkit-line-clamp: $lines;
47
+ line-clamp: $lines;
48
+ overflow: hidden;
49
+ }
@@ -4,6 +4,13 @@
4
4
  @use "sass:meta";
5
5
  @use "../utilities/is-time" as *;
6
6
 
7
+ // `init` defines the %loadify placeholder, and every call extends it. Under the
8
+ // module system a selector can only extend a placeholder from a module it
9
+ // loads, so the file with the call must be the file that calls `init`, or must
10
+ // `@use` it. Anything else fails with "The target selector was not found".
11
+ // An entry file calling `init` does not help a partial it loads, since the
12
+ // partial does not load the entry file. Stylesheets using @import share one
13
+ // scope and are unaffected.
7
14
  @mixin loadify($params...) {
8
15
  @if not & {
9
16
  @if list.length($params) == 0 or (list.length($params) == 1 and list.nth($params, 1) == "init") {
@@ -20,6 +27,17 @@
20
27
  backface-visibility: hidden;
21
28
  animation-name: loadify;
22
29
  animation-fill-mode: forwards;
30
+
31
+ // The element starts invisible and the animation is what reveals it,
32
+ // so switching the animation off is not enough on its own: it would
33
+ // leave the content hidden for good. Under reduced motion the end
34
+ // state is applied directly instead, and nothing moves.
35
+ @media (prefers-reduced-motion: reduce) {
36
+ opacity: 1;
37
+ visibility: visible;
38
+ backface-visibility: visible;
39
+ animation: none;
40
+ }
23
41
  }
24
42
  } @else if (list.length($params) == 1 and list.nth($params, 1) != "init") or (list.length($params) == 1 and meta.type-of(list.nth($params, 1)) != "string") {
25
43
  @error "#{list.nth($params, 1)} is not a valid argument. Please pass `init` as an argument to initialize the effect or do not pass any argument at all.";
@@ -4,10 +4,12 @@
4
4
  @if & {
5
5
  @error "Please call the mixin at the root of your style sheet.";
6
6
  } @else {
7
- /* http://meyerweb.com/eric/tools/css/reset/
8
- v2.0 | 20110126
9
- License: none (public domain)
10
- */
7
+ // http://meyerweb.com/eric/tools/css/reset/
8
+ // v2.0 | 20110126
9
+ // License: none (public domain)
10
+ //
11
+ // Comments in this file are `//`: a /* */ comment inside a mixin is emitted
12
+ // into the stylesheet of everyone who includes it.
11
13
  html, body, div, span, applet, object, iframe,
12
14
  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
13
15
  a, abbr, acronym, address, big, cite, code,
@@ -28,7 +30,7 @@
28
30
  font: inherit;
29
31
  vertical-align: baseline;
30
32
  }
31
- /* HTML5 display-role reset for older browsers */
33
+ // HTML5 display-role reset for older browsers
32
34
  article, aside, details, figcaption, figure,
33
35
  footer, header, hgroup, menu, main, nav, section {
34
36
  display: block;
@@ -1,6 +1,33 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // Returns the first value in $value that holds var(), looking inside lists and
8
+ // calculations, or null. Custom properties are not evaluated in a @media or
9
+ // @container condition, so a rule built from one never applies, and nothing
10
+ // warns. Measured in Chrome 152: `(min-width: var(--wide))` did not match with
11
+ // `--wide: 200px`, while `(min-width: 200px)` did.
12
+ @function -custom-property-in($value) {
13
+ @each $item in $value {
14
+ @if meta.type-of($item) == "list" or meta.type-of($item) == "arglist" {
15
+ $found: -custom-property-in($item);
16
+ @if $found {
17
+ @return $found;
18
+ }
19
+ } @else if string.index(meta.inspect($item), "var(") {
20
+ @return $item;
21
+ }
22
+ }
23
+ @return null;
24
+ }
25
+
3
26
  @mixin screen-agent($resolution) {
27
+ $custom-property: -custom-property-in($resolution);
28
+ @if $custom-property {
29
+ @error "`#{$custom-property}` cannot be used as a resolution: custom properties are not evaluated in a @media condition, so the rule would never apply. Pass `1x`, `2x`, `3x` or a resolution such as `2dppx`.";
30
+ }
4
31
  @if $resolution == "1x" {
5
32
  @media (min-resolution: 96dpi) {
6
33
  @content;
@@ -1,8 +1,31 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // An image path goes into url(). A var(), url(), image-set() or gradient is
8
+ // already an image, and wrapping it produced url(var(--x)), which loads nothing.
9
+ @function -image($value) {
10
+ @if -is-css-function($value, "var" "url" "image-set" "linear-gradient" "radial-gradient" "conic-gradient" "repeating-linear-gradient" "repeating-radial-gradient" "repeating-conic-gradient") {
11
+ @return $value;
12
+ }
13
+ @return url($value);
14
+ }
15
+
16
+ // True for an unquoted call to one of $names, such as var(--x). A quoted string
17
+ // stays text, so `"var(--x)"` is not a call.
18
+ @function -is-css-function($value, $names) {
19
+ @if meta.type-of($value) != "string" or string.slice(meta.inspect($value), 1, 1) == "\"" {
20
+ @return false;
21
+ }
22
+ $open: string.index($value, "(");
23
+ @return $open != null and list.index($names, string.to-lower-case(string.slice($value, 1, $open - 1))) != null;
24
+ }
25
+
3
26
  @mixin text-image($image: null) {
4
27
  @if $image {
5
- background-image: url($image);
28
+ background-image: -image($image);
6
29
  }
7
30
  background-size: cover;
8
31
  background-position: center;
@@ -1,13 +1,75 @@
1
1
  @charset "UTF-8";
2
2
 
3
3
  @use "sass:list";
4
- @use "sass:math";
4
+ @use "sass:meta";
5
+ @use "sass:string";
5
6
  @use "../lists/list-of-directions" as *;
6
- @use "../utilities/is-color" as *;
7
+
8
+ // A size Sass can use inside calc(): a number, a calculation such as calc(),
9
+ // min() or clamp(), or var() and env(), which only the browser can resolve.
10
+ // Anything else would pass silently through calc(): `calc(nonsense * 5)`
11
+ // compiles.
12
+ @function -is-size($value) {
13
+ @if meta.type-of($value) == "number" or meta.type-of($value) == "calculation" {
14
+ @return true;
15
+ }
16
+ @if meta.type-of($value) == "string" and string.slice(meta.inspect($value), 1, 1) != "\"" {
17
+ $open: string.index($value, "(");
18
+ @return $open != null and list.index("var" "env", string.to-lower-case(string.slice($value, 1, $open - 1))) != null;
19
+ }
20
+ @return false;
21
+ }
22
+
23
+ // A value CSS will take as a colour: a Sass colour, currentColor, or a function
24
+ // that returns a colour when the browser resolves it. A list passes when every
25
+ // item does, as it did through isColor.
26
+ //
27
+ // inherit, initial, unset and revert are left out on purpose. The mixin writes
28
+ // the colour into a border-color shorthand beside `transparent`, and a CSS-wide
29
+ // keyword is only valid as the whole value: `transparent transparent inherit`
30
+ // is rejected by the browser. Checked with CSS.supports in Chrome 152.
31
+ //
32
+ // Deliberately looser than isColor, which stays strict because tint and shade
33
+ // pass its result to color.mix, and that needs a real Sass colour. A border
34
+ // colour does not. The functions are named rather than "anything with a
35
+ // parenthesis", which would let url() through as a colour.
36
+ @function -is-css-color($value) {
37
+ @if list.length($value) > 1 {
38
+ @each $item in $value {
39
+ @if not -is-css-color($item) {
40
+ @return false;
41
+ }
42
+ }
43
+ @return true;
44
+ }
45
+ @if meta.type-of($value) == "color" {
46
+ @return true;
47
+ }
48
+ @if meta.type-of($value) == "string" and string.slice(meta.inspect($value), 1, 1) != "\"" {
49
+ $text: string.to-lower-case($value);
50
+ @if $text == "currentcolor" {
51
+ @return true;
52
+ }
53
+ $open: string.index($text, "(");
54
+ @if $open and list.index("var" "color-mix" "light-dark" "env", string.slice($text, 1, $open - 1)) {
55
+ @return true;
56
+ }
57
+ }
58
+ @return false;
59
+ }
7
60
 
8
61
  @mixin triangle($direction: "bottom", $color: black, $size: 10px 8px) {
9
62
  @if list.index($list-of-directions, $direction) {
10
- @if isColor($color){
63
+ // Halving a size in calc() rather than math.div() keeps var() and clamp()
64
+ // working instead of emitting `var(--w)/2`, which is not valid CSS.
65
+ @each $item in $size {
66
+ @if not -is-size($item) {
67
+ @error "`#{meta.inspect($size)}` is not a valid $size for `triangle`. Pass one or two lengths, or var(), env() or a calculation in place of either.";
68
+ }
69
+ }
70
+ @if not -is-css-color($color) {
71
+ @error "`#{meta.inspect($color)}` is not a valid $color for `triangle`. Pass a colour, `currentColor`, or a CSS function such as `var(--accent)`, `color-mix(...)` or `light-dark(...)`.";
72
+ } @else {
11
73
  content: "";
12
74
  height: 0;
13
75
  width: 0;
@@ -15,25 +77,25 @@
15
77
  border-style: solid;
16
78
  @if $direction == "top"{
17
79
  border-color: transparent transparent $color;
18
- border-width: 0 math.div(list.nth($size, 1), 2) if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1));
80
+ border-width: 0 calc(list.nth($size, 1) / 2) if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1));
19
81
  } @else if $direction == "top-right" {
20
82
  border-color: transparent $color transparent transparent;
21
83
  border-width: 0 list.nth($size, 1) list.nth($size, 1) 0;
22
84
  } @else if $direction == "right" {
23
85
  border-color: transparent transparent transparent $color;
24
- border-width: math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) 0 math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) list.nth($size, 1);
86
+ border-width: calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) 0 calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) list.nth($size, 1);
25
87
  } @else if $direction == "bottom-right" {
26
88
  border-color: transparent transparent $color;
27
89
  border-width: 0 0 list.nth($size, 1) list.nth($size, 1);
28
90
  } @else if $direction == "bottom" {
29
91
  border-color: $color transparent transparent;
30
- border-width: if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) math.div(list.nth($size, 1), 2) 0;
92
+ border-width: if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) calc(list.nth($size, 1) / 2) 0;
31
93
  } @else if $direction == "bottom-left" {
32
94
  border-color: transparent transparent transparent $color;
33
95
  border-width: list.nth($size, 1) 0 0 list.nth($size, 1);
34
96
  } @else if $direction == "left" {
35
97
  border-color: transparent $color transparent transparent;
36
- border-width: math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) list.nth($size, 1) math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) 0;
98
+ border-width: calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) list.nth($size, 1) calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) 0;
37
99
  } @else if $direction == "top-left" {
38
100
  border-color: $color transparent transparent;
39
101
  border-width: list.nth($size, 1) list.nth($size, 1) 0 0;
@@ -1,7 +1,11 @@
1
1
  @charset "UTF-8";
2
2
 
3
3
  @use "sass:math";
4
+ @use "sass:meta";
4
5
 
5
6
  @function clearUnit($value) {
7
+ @if meta.type-of($value) != "number" {
8
+ @error "`#{meta.inspect($value)}` is not a valid $value for `clearUnit`. Pass a number, such as `24px`.";
9
+ }
6
10
  @return math.div($value, ($value * 0 + 1));
7
11
  }
@@ -2,8 +2,14 @@
2
2
 
3
3
  @use "sass:string";
4
4
  @use "sass:math";
5
+ @use "sass:meta";
5
6
 
6
7
  @function convertToEm($value) {
8
+ // Only a pixel length converts cleanly. A unitless number or another unit
9
+ // compiled to `calc(1.5 / 1px)em`, and a word or var() to `var(--x)/16pxem`.
10
+ @if meta.type-of($value) != "number" or math.unit($value) != "px" {
11
+ @error "`#{meta.inspect($value)}` is not a valid $value for `convertToEm`. Pass a pixel length, such as `24px`.";
12
+ }
7
13
  $base-value: 16px;
8
14
  $rem-value: math.div($value, $base-value);
9
15
  @return $rem-value + string.unquote("em");
@@ -11,6 +11,21 @@
11
11
  }
12
12
 
13
13
  $string-numerals: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
14
+
15
+ // Every character has to be a digit before the parse runs. Otherwise an
16
+ // empty string or a sign failed with Sass's own "Undefined operation", and a
17
+ // letter or a space was folded into nonsense such as `40-1`. The empty case
18
+ // is checked first because `@for $i from 1 through 0` counts down, not zero
19
+ // times.
20
+ @if string.length($value) == 0 {
21
+ @error "`\"\"` is not a valid $value for `convertToNumber`. Pass a string made only of digits, such as `\"42\"`.";
22
+ }
23
+ @for $i from 1 through string.length($value) {
24
+ @if not list.index($string-numerals, string.slice($value, $i, $i)) {
25
+ @error "`#{meta.inspect($value)}` is not a valid $value for `convertToNumber`. Pass a string made only of digits, such as `\"42\"`.";
26
+ }
27
+ }
28
+
14
29
  $calc: 0;
15
30
 
16
31
  @for $i from 1 through string.length($value) {
@@ -0,0 +1,76 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:math";
5
+ @use "sass:meta";
6
+ @use "sass:string";
7
+ @use "../utilities/clear-unit" as *;
8
+ @use "../utilities/remify" as *;
9
+
10
+ // A value that grows with the viewport between two widths, then stops.
11
+ //
12
+ // clamp() takes a floor, a preferred value and a ceiling. The preferred value
13
+ // is the straight line through (min-viewport, min) and (max-viewport, max):
14
+ //
15
+ // slope = (max - min) / (max-viewport - min-viewport)
16
+ // intercept = min - slope * min-viewport
17
+ // preferred = intercept + slope * 100vw
18
+ //
19
+ // The intercept is emitted in rem rather than px deliberately. A preferred
20
+ // value made only of vw ignores browser zoom, which fails WCAG 1.4.4, and
21
+ // keeping a rem term in the sum is what restores it. This is the part that is
22
+ // easy to get wrong by hand, and the reason this is a function rather than a
23
+ // mixin: the value is the hard part, and it belongs to any property, not just
24
+ // font-size.
25
+ //
26
+ // Sass cannot add rem to vw, so the sum is assembled as a string. CSS evaluates
27
+ // it; Sass only writes it.
28
+
29
+ $_fluid-root-size: 16px;
30
+
31
+ @function fluid($min, $max, $min-viewport: 320px, $max-viewport: 1280px) {
32
+ // Pairs rather than a map: two arguments can legitimately hold the same
33
+ // length, and a map would reject that as a duplicate key.
34
+ $given: (($min, "$min"), ($max, "$max"), ($min-viewport, "$min-viewport"), ($max-viewport, "$max-viewport"));
35
+ @each $pair in $given {
36
+ $value: list.nth($pair, 1);
37
+ $label: list.nth($pair, 2);
38
+ @if meta.type-of($value) != "number" or math.is-unitless($value) {
39
+ @error "`#{$value}` is not a valid #{$label} for `fluid`. Pass a length in px or rem.";
40
+ }
41
+ @if math.unit($value) != "px" and math.unit($value) != "rem" {
42
+ @error "`#{$value}` is not a valid #{$label} for `fluid`. Pass a length in px or rem, not #{math.unit($value)}.";
43
+ }
44
+ }
45
+
46
+ // Everything is compared in px, because Sass cannot convert rem to px on its
47
+ // own: it has no way to know the root font size, so the assumption is stated
48
+ // here rather than hidden.
49
+ $min-px: $min;
50
+ $max-px: $max;
51
+ $from-px: $min-viewport;
52
+ $to-px: $max-viewport;
53
+ @if math.unit($min) == "rem" { $min-px: clearUnit($min) * $_fluid-root-size; }
54
+ @if math.unit($max) == "rem" { $max-px: clearUnit($max) * $_fluid-root-size; }
55
+ @if math.unit($min-viewport) == "rem" { $from-px: clearUnit($min-viewport) * $_fluid-root-size; }
56
+ @if math.unit($max-viewport) == "rem" { $to-px: clearUnit($max-viewport) * $_fluid-root-size; }
57
+
58
+ @if $from-px >= $to-px {
59
+ @error "`fluid` needs $min-viewport to be smaller than $max-viewport, and was given #{$min-viewport} and #{$max-viewport}.";
60
+ }
61
+ @if $min-px > $max-px {
62
+ @error "`fluid` needs $min to be no larger than $max, and was given #{$min} and #{$max}. clamp() would return the floor at every width, so the value would never grow.";
63
+ }
64
+
65
+ $slope: math.div($max-px - $min-px, $to-px - $from-px);
66
+ $intercept: $min-px - $slope * $from-px;
67
+
68
+ // Four decimals is past the point any browser renders differently, and keeps
69
+ // the output readable.
70
+ $vw: math.div(math.round($slope * 100 * 10000), 10000);
71
+ $floor: math.div(math.round(clearUnit(remify($min-px)) * 10000), 10000);
72
+ $base: math.div(math.round(clearUnit(remify($intercept)) * 10000), 10000);
73
+ $ceiling: math.div(math.round(clearUnit(remify($max-px)) * 10000), 10000);
74
+
75
+ @return string.unquote("clamp(#{$floor}rem, #{$base}rem + #{$vw}vw, #{$ceiling}rem)");
76
+ }
@@ -1,5 +1,19 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:math";
4
+ @use "sass:meta";
5
+
3
6
  @function fontSizer($size, $time) {
7
+ @if meta.type-of($size) != "number" {
8
+ @error "`#{meta.inspect($size)}` is not a valid $size for `fontSizer`. Pass a length, such as `16px`.";
9
+ }
10
+ @if meta.type-of($time) != "number" {
11
+ @error "`#{meta.inspect($time)}` is not a valid $time for `fontSizer`. Pass a multiplier, such as `1.5`.";
12
+ }
13
+ // Two units multiply into a unit squared, which Sass writes as
14
+ // `calc(16px * 1px)`: not a length anywhere in CSS.
15
+ @if not math.is-unitless($size) and not math.is-unitless($time) {
16
+ @error "`fontSizer(#{$size}, #{$time})` multiplies two units, which is not a length. Pass the multiplier without a unit, such as `1.5`.";
17
+ }
4
18
  @return $size * $time;
5
19
  }
@@ -5,6 +5,7 @@
5
5
  @forward "convert-to-em";
6
6
  @forward "convert-to-number";
7
7
  @forward "fill-nulls";
8
+ @forward "fluid";
8
9
  @forward "font-sizer";
9
10
  @forward "font-source";
10
11
  @forward "is-color";
@@ -1,8 +1,15 @@
1
1
  @charset "UTF-8";
2
2
 
3
3
  @use "sass:math";
4
+ @use "sass:meta";
4
5
 
5
6
  @function remify($value) {
7
+ // Only a pixel length converts cleanly. Anything else used to reach the
8
+ // arithmetic unchecked: a word failed with Sass's own "Undefined operation",
9
+ // and a unitless number or another unit compiled to `calc(1.5rem / 1px)`.
10
+ @if meta.type-of($value) != "number" or math.unit($value) != "px" {
11
+ @error "`#{meta.inspect($value)}` is not a valid $value for `remify`. Pass a pixel length, such as `24px`.";
12
+ }
6
13
  $base-value: 16px;
7
14
  $rem-value: math.div($value, $base-value);
8
15
  @return $rem-value * 1rem;
@@ -6,6 +6,13 @@
6
6
  @use "sass:string";
7
7
 
8
8
  @function validateLength($value) {
9
+ // null means "leave this out": `position` skips an edge given null, as its
10
+ // documentation shows. It is not a mistake to warn about, and the warning
11
+ // printed empty backticks, because null interpolates to nothing.
12
+ @if $value == null {
13
+ @return null;
14
+ }
15
+
9
16
  $keywords: ("auto", "inherit", "initial", "unset", "revert", 0);
10
17
 
11
18
  @if list.index($keywords, $value) {
@@ -28,6 +28,13 @@
28
28
  @return $ratio;
29
29
  }
30
30
 
31
+ // var() is resolved by the browser. Checked inline rather than through a
32
+ // helper function, which tools/build-manifest.js would take for this file's
33
+ // member in place of validateRatio.
34
+ @if meta.type-of($ratio) == "string" and string.slice(meta.inspect($ratio), 1, 1) != "\"" and string.index(string.to-lower-case($ratio), "var(") == 1 {
35
+ @return $ratio;
36
+ }
37
+
31
38
  @if meta.type-of($ratio) == "string" {
32
39
  $separator: null;
33
40
  @if string.index($ratio, "/") {
@@ -38,8 +45,24 @@
38
45
  @error "`#{$ratio}` is not a valid ratio. A string ratio must separate width and height with `/` or `:`, like `\"16/9\"` or `\"16:9\"`.";
39
46
  }
40
47
 
41
- $width: convertToNumber(string.slice($ratio, 1, $separator - 1));
42
- $height: convertToNumber(string.slice($ratio, $separator + 1));
48
+ $width-text: string.slice($ratio, 1, $separator - 1);
49
+ $height-text: string.slice($ratio, $separator + 1);
50
+
51
+ // Checked here so a malformed ratio is reported as a ratio, rather than as
52
+ // a bad argument to convertToNumber, which the caller never called.
53
+ @each $part in ($width-text, $height-text) {
54
+ @if string.length($part) == 0 {
55
+ @error "`#{$ratio}` is not a valid ratio. Width and height must both be whole numbers, like `\"16/9\"` or `\"16:9\"`.";
56
+ }
57
+ @for $i from 1 through string.length($part) {
58
+ @if not list.index("0" "1" "2" "3" "4" "5" "6" "7" "8" "9", string.slice($part, $i, $i)) {
59
+ @error "`#{$ratio}` is not a valid ratio. Width and height must both be whole numbers, like `\"16/9\"` or `\"16:9\"`.";
60
+ }
61
+ }
62
+ }
63
+
64
+ $width: convertToNumber($width-text);
65
+ $height: convertToNumber($height-text);
43
66
 
44
67
  @if $width == 0 {
45
68
  @error "`#{$ratio}` is not a valid ratio. The width must be greater than zero.";