sass-func 1.0.0
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.
- package/README.md +270 -0
- package/list.scss +229 -0
- package/meta.scss +72 -0
- package/number.scss +159 -0
- package/package.json +46 -0
- package/string.scss +145 -0
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# sass-func
|
|
2
|
+
|
|
3
|
+
A collection of Sass utility functions for [lists](#module-list), [strings](#module-string), [numbers](#module-number), and [meta](#module-meta) utilities, inspired by familiar JS APIs. No dependencies beyond the Sass built-in modules. Where behaviour differs from a JS counterpart, the difference is noted.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install sass-func
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> Requires `sass >= 1.33.0` or `sass-embedded >= 1.33.0` — install one, not both. `sass-embedded` is recommended: it wraps a native Dart binary and is typically 20–30% faster to compile than the pure-JS `sass` package.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Modules
|
|
18
|
+
|
|
19
|
+
| Module | Import path | Description |
|
|
20
|
+
| ------------------- | ----------------------------------- | -------------------------------------------------------- |
|
|
21
|
+
| [`list`](#module-list) | [`sass-func/list`](./list.scss) | Functions for creating, transforming, and querying lists |
|
|
22
|
+
| [`meta`](#module-meta) | [`sass-func/meta`](./meta.scss) | Functions for type inspection and emptiness checking |
|
|
23
|
+
| [`number`](#module-number) | [`sass-func/number`](./number.scss) | Functions for unit conversion and numeric operations |
|
|
24
|
+
| [`string`](#module-string) | [`sass-func/string`](./string.scss) | Functions for string transformation and parsing |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Module: List
|
|
29
|
+
|
|
30
|
+
```scss
|
|
31
|
+
@use 'sass-func/list' as l;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `concat($lists...)`
|
|
35
|
+
|
|
36
|
+
Concatenates zero or more lists into a single flat list.
|
|
37
|
+
|
|
38
|
+
```scss
|
|
39
|
+
l.concat((a, b), (c, d)) // → a, b, c, d
|
|
40
|
+
l.concat() // → ()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `flat($list, $depth: null)`
|
|
44
|
+
|
|
45
|
+
Recursively flattens nested lists. Pass `$depth` to limit how many levels deep the flattening goes; omit it (or pass `null`) for unlimited depth. Similar to `Array.prototype.flat(depth)`.
|
|
46
|
+
|
|
47
|
+
> **Default depth differs:** Sass defaults to `null` (unlimited flattening); JS defaults to `1`.
|
|
48
|
+
|
|
49
|
+
```scss
|
|
50
|
+
l.flat((a, (b, (c, d)))) // → a, b, c, d
|
|
51
|
+
l.flat((a, (b, (c, d))), 1) // → a, b, (c, d)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `includes($list, $value)`
|
|
55
|
+
|
|
56
|
+
Returns `true` if `$value` is present in `$list`, `false` otherwise. Similar to `Array.prototype.includes(value)`.
|
|
57
|
+
|
|
58
|
+
```scss
|
|
59
|
+
l.includes((a, b, c), b) // → true
|
|
60
|
+
l.includes((a, b, c), z) // → false
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `join($list, $separator: ', ')`
|
|
64
|
+
|
|
65
|
+
Serializes a list into a string, inserting `$separator` between elements. Similar to `Array.prototype.join(separator)`.
|
|
66
|
+
|
|
67
|
+
> **Default separator differs:** Sass defaults to `', '` (comma + space); JS defaults to `','` (comma only).
|
|
68
|
+
|
|
69
|
+
```scss
|
|
70
|
+
l.join((a, b, c)) // → 'a, b, c'
|
|
71
|
+
l.join((a, b, c), ' | ') // → 'a | b | c'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `push($list, $val, $separator: comma)`
|
|
75
|
+
|
|
76
|
+
Appends `$val` to the end of `$list`. `$separator` controls the resulting list's separator: `comma`, `space`, `slash`, or `auto`. Similar to `Array.prototype.push(val)` — unlike JS, this returns a new list rather than mutating in place.
|
|
77
|
+
|
|
78
|
+
```scss
|
|
79
|
+
l.push((a, b), c) // → a, b, c
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `slice($list, $start: null, $end: null)`
|
|
83
|
+
|
|
84
|
+
Returns the sublist from index `$start` to `$end` (1-based, inclusive). Omitting either bound defaults to the start or end of the list. Similar to `Array.prototype.slice(start, end)`.
|
|
85
|
+
|
|
86
|
+
> **Index base and bounds differ:** Sass uses 1-based indices with an inclusive end; JS uses 0-based indices with an exclusive end. `slice(list, 2, 3)` returns elements 2 and 3; the JS equivalent would be `array.slice(1, 3)`.
|
|
87
|
+
|
|
88
|
+
```scss
|
|
89
|
+
l.slice((a, b, c, d), 2, 3) // → b, c
|
|
90
|
+
l.slice((a, b, c, d), 3) // → c, d
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `sort($list, $desc: false)`
|
|
94
|
+
|
|
95
|
+
Sorts a list alphabetically (case-insensitive). Numbers sort by value. Pass `$desc: true` for descending order. Similar to `Array.prototype.sort()`.
|
|
96
|
+
|
|
97
|
+
> **Intentional differences:** Numbers sort by value (JS sorts lexicographically by default, so `[10, 2, 1].sort()` → `[1, 10, 2]` in JS). Sorting is also case-insensitive; JS is case-sensitive by default.
|
|
98
|
+
|
|
99
|
+
```scss
|
|
100
|
+
l.sort((c, a, b)) // → a, b, c
|
|
101
|
+
l.sort((c, a, b), $desc: true) // → c, b, a
|
|
102
|
+
l.sort((3, 1, 2)) // → 1, 2, 3
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Module: Meta
|
|
108
|
+
|
|
109
|
+
```scss
|
|
110
|
+
@use 'sass-func/meta' as m;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `is-empty($value, $depth: 0)`
|
|
114
|
+
|
|
115
|
+
Returns `true` if `$value` is empty. Covers `null`, blank strings, empty lists, and empty maps. Pass `$depth > 0` or `null` to also check nested structures. Similar to `_.isEmpty(value)` (lodash) — extended with configurable recursion depth.
|
|
116
|
+
|
|
117
|
+
> **Whitespace strings:** A string containing only spaces (e.g. `' '`) is considered empty. JS `_.isEmpty(' ')` returns `false`.
|
|
118
|
+
|
|
119
|
+
```scss
|
|
120
|
+
m.is-empty(null) // → true
|
|
121
|
+
m.is-empty('') // → true
|
|
122
|
+
m.is-empty(' ') // → true (whitespace-only)
|
|
123
|
+
m.is-empty(()) // → true
|
|
124
|
+
m.is-empty((a: ())) // → false (shallow)
|
|
125
|
+
m.is-empty((a: ()), $depth: 1) // → true (checks nested)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `is-type($value, $types)`
|
|
129
|
+
|
|
130
|
+
Returns `true` if `$value`'s type is in the `$types` list.
|
|
131
|
+
|
|
132
|
+
```scss
|
|
133
|
+
m.is-type(1rem, ('number', 'string')) // → true
|
|
134
|
+
m.is-type(true, ('number', 'string')) // → false
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `type-of($value)`
|
|
138
|
+
|
|
139
|
+
Returns the Sass type name of any value. A thin wrapper over `meta.type-of` — included for consistent import paths within this package. Similar to `typeof value` — returns Sass-specific type names (`'number'`, `'string'`, `'map'`, `'list'`, `'color'`, `'bool'`, `'null'`).
|
|
140
|
+
|
|
141
|
+
```scss
|
|
142
|
+
m.type-of(1px) // → 'number'
|
|
143
|
+
m.type-of('hello') // → 'string'
|
|
144
|
+
m.type-of((a, b)) // → 'list'
|
|
145
|
+
m.type-of((a: 1)) // → 'map'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Module: Number
|
|
151
|
+
|
|
152
|
+
```scss
|
|
153
|
+
@use 'sass-func/number' as n;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `$SUPPORTED_LENGTH_UNITS`
|
|
157
|
+
|
|
158
|
+
A list of the CSS length units supported by `add-unit` and `convert-unit`:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
px, rem, em, cm, mm, Q, in, pc, pt
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `add-unit($number, $unit)`
|
|
165
|
+
|
|
166
|
+
Attaches a CSS length unit to a unitless number.
|
|
167
|
+
|
|
168
|
+
```scss
|
|
169
|
+
n.add-unit(16, 'px') // → 16px
|
|
170
|
+
n.add-unit(1, 'rem') // → 1rem
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `convert-unit($number, $from-unit, $to-unit)`
|
|
174
|
+
|
|
175
|
+
Converts a number from one CSS length unit to another.
|
|
176
|
+
|
|
177
|
+
```scss
|
|
178
|
+
n.convert-unit(16px, 'px', 'rem') // → 1rem
|
|
179
|
+
n.convert-unit(1in, 'in', 'px') // → 96px
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `resolve-index($index, $length)`
|
|
183
|
+
|
|
184
|
+
Resolves a positive or negative integer index to a 1-based index within a collection of `$length`. Returns `null` if out of bounds. Negative indices count from the end, matching Python and modern JS convention.
|
|
185
|
+
|
|
186
|
+
> **1-based output:** Returns a 1-based index, matching Sass's index convention. Negative indices resolve relative to the end — `-1` on a length-5 list returns `5` (the last position), not `4`.
|
|
187
|
+
|
|
188
|
+
```scss
|
|
189
|
+
n.resolve-index(2, 5) // → 2
|
|
190
|
+
n.resolve-index(-1, 5) // → 5
|
|
191
|
+
n.resolve-index(0, 5) // → null
|
|
192
|
+
n.resolve-index(6, 5) // → null
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `to-fixed($number, $digits: 0)`
|
|
196
|
+
|
|
197
|
+
Rounds a number to `$digits` decimal places. Similar to `Number.prototype.toFixed(digits)` — returns a number, not a string.
|
|
198
|
+
|
|
199
|
+
```scss
|
|
200
|
+
n.to-fixed(3.14159, 2) // → 3.14
|
|
201
|
+
n.to-fixed(1.5) // → 2
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Module: String
|
|
207
|
+
|
|
208
|
+
```scss
|
|
209
|
+
@use 'sass-func/string' as s;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `capitalize($string)`
|
|
213
|
+
|
|
214
|
+
Capitalizes the first character and lowercases the rest. Similar to `_.capitalize(string)` (lodash).
|
|
215
|
+
|
|
216
|
+
> **Input is trimmed first:** Leading and trailing spaces are stripped before capitalizing. JS lodash preserves them.
|
|
217
|
+
|
|
218
|
+
```scss
|
|
219
|
+
s.capitalize('hello world') // → 'Hello world'
|
|
220
|
+
s.capitalize('HELLO') // → 'Hello'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `replace-all($string, $pattern, $replacement: '')`
|
|
224
|
+
|
|
225
|
+
Replaces every occurrence of `$pattern` in `$string` with `$replacement`. Defaults to deletion if `$replacement` is omitted. Similar to `String.prototype.replaceAll(pattern, replacement)`.
|
|
226
|
+
|
|
227
|
+
```scss
|
|
228
|
+
s.replace-all('foo bar foo', 'foo', 'baz') // → 'baz bar baz'
|
|
229
|
+
s.replace-all('a--b--c', '--') // → 'abc'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `slugify($string)`
|
|
233
|
+
|
|
234
|
+
Converts a string to a URL-safe slug: lowercased, spaces replaced with hyphens, special characters removed. Similar to `_.kebabCase(string)` (lodash) — with stricter special-character removal.
|
|
235
|
+
|
|
236
|
+
```scss
|
|
237
|
+
s.slugify('Hello World!') // → 'hello-world'
|
|
238
|
+
s.slugify('Font Size (lg)') // → 'font-size-lg'
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `split($string, $separator: null)`
|
|
242
|
+
|
|
243
|
+
Splits a string into a list using `$separator`. An empty string splits into individual characters. `null` returns the string wrapped in a single-element list. Similar to `String.prototype.split(separator)`.
|
|
244
|
+
|
|
245
|
+
```scss
|
|
246
|
+
s.split('a,b,c', ',') // → a, b, c
|
|
247
|
+
s.split('abc', '') // → a, b, c
|
|
248
|
+
s.split('abc') // → 'abc'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `trim($string)`
|
|
252
|
+
|
|
253
|
+
Removes leading and trailing whitespace. Similar to `String.prototype.trim()`.
|
|
254
|
+
|
|
255
|
+
> **Space characters only:** Strips the space character `' '`. JS strips all whitespace (`\t`, `\n`, `\r`, etc.) — Sass has no native regex equivalent for this.
|
|
256
|
+
|
|
257
|
+
```scss
|
|
258
|
+
s.trim(' hello ') // → 'hello'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `upper-snake-case($string)`
|
|
262
|
+
|
|
263
|
+
Converts a string to `UPPER_SNAKE_CASE` by replacing hyphens and spaces with underscores.
|
|
264
|
+
|
|
265
|
+
```scss
|
|
266
|
+
s.upper-snake-case('font-size') // → 'FONT_SIZE'
|
|
267
|
+
s.upper-snake-case('my variable') // → 'MY_VARIABLE'
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
[Back to top](#sass-func)
|
package/list.scss
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
@use 'sass:list';
|
|
2
|
+
@use 'sass:meta';
|
|
3
|
+
@use 'sass:math';
|
|
4
|
+
@use 'sass:string';
|
|
5
|
+
|
|
6
|
+
// PUBLIC API ──────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/// Concatenate zero or more lists into a single flat list
|
|
9
|
+
/// @param {ArgList} $lists... - Zero or more lists to concatenate
|
|
10
|
+
/// @return {List} A single flat list containing all elements
|
|
11
|
+
@function concat($lists...) {
|
|
12
|
+
$result: ();
|
|
13
|
+
@each $list in $lists {
|
|
14
|
+
$result: list.join($result, $list, $separator: auto, $bracketed: auto);
|
|
15
|
+
}
|
|
16
|
+
@return $result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Recursively flatten nested lists to $depth levels deep, or fully if null
|
|
20
|
+
/// @param {List | ArgList} $list - The list to flatten
|
|
21
|
+
/// @param {Number | Null} $depth [null] - Flattening depth; null means unlimited
|
|
22
|
+
/// @return {List} Flattened list
|
|
23
|
+
@function flat($list, $depth: null) {
|
|
24
|
+
@if not list.index(('list', 'arglist'), meta.type-of($list)) {
|
|
25
|
+
@error '$list: #{meta.inspect($list)} is not a list or arglist.';
|
|
26
|
+
} @else if $depth != null and meta.type-of($depth) != 'number' {
|
|
27
|
+
@error '$depth: #{meta.inspect($depth)} is not a number or null.';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Depth limit reached — return the list as-is
|
|
31
|
+
@if $depth == 0 {
|
|
32
|
+
@return $list;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$result: ();
|
|
36
|
+
@each $element in $list {
|
|
37
|
+
@if meta.type-of($element) != 'list' {
|
|
38
|
+
$result: push($result, $element);
|
|
39
|
+
} @else if $depth == 1 {
|
|
40
|
+
// Depth of 1 — inline sub-list elements directly without recursing
|
|
41
|
+
@each $sub-element in $element {
|
|
42
|
+
$result: push($result, $sub-element);
|
|
43
|
+
}
|
|
44
|
+
} @else {
|
|
45
|
+
// Recurse with decremented depth, or null for unlimited
|
|
46
|
+
$next-depth: null;
|
|
47
|
+
@if $depth == null {
|
|
48
|
+
$next-depth: null;
|
|
49
|
+
} @else {
|
|
50
|
+
$next-depth: $depth - 1;
|
|
51
|
+
}
|
|
52
|
+
$sub-list: flat($element, $next-depth);
|
|
53
|
+
@each $sub-element in $sub-list {
|
|
54
|
+
$result: push($result, $sub-element);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
@return $result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Check whether $value is present in $list
|
|
62
|
+
/// @param {List} $list - The list to search
|
|
63
|
+
/// @param {*} $value - The value to look for
|
|
64
|
+
/// @return {Bool} `true` if found, `false` otherwise
|
|
65
|
+
@function includes($list, $value) {
|
|
66
|
+
@return list.index($list, $value) != null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Serialize $list elements into a string with $separator between each
|
|
70
|
+
/// @param {List | ArgList} $list - The list to join
|
|
71
|
+
/// @param {String} $separator [', '] - String inserted between elements
|
|
72
|
+
/// @return {String} Joined string
|
|
73
|
+
@function join($list, $separator: ', ') {
|
|
74
|
+
@if not list.index(('list', 'arglist'), meta.type-of($list)) {
|
|
75
|
+
@error '$list: #{meta.inspect($list)} is not a list or arglist.';
|
|
76
|
+
} @else if meta.type-of($separator) != 'string' {
|
|
77
|
+
@error '$separator: #{meta.inspect($separator)} is not a string.';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$result: '';
|
|
81
|
+
@each $element in $list {
|
|
82
|
+
@if $result == '' {
|
|
83
|
+
$result: $element;
|
|
84
|
+
} @else {
|
|
85
|
+
$result: $result + $separator + $element;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
@return $result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Append $val to the end of $list
|
|
92
|
+
/// @param {List} $list - The list to append to
|
|
93
|
+
/// @param {*} $val - The value to append
|
|
94
|
+
/// @param {String} $separator [comma] - List separator; `comma`, `space`, `slash`, or `auto`
|
|
95
|
+
/// @return {List} New list with $val appended
|
|
96
|
+
@function push($list, $val, $separator: comma) {
|
|
97
|
+
$allowed-separators: (comma, space, slash, auto);
|
|
98
|
+
@if list.index($allowed-separators, $separator) == null {
|
|
99
|
+
@error '$separator: Must be "space", "comma", "slash", or "auto".';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Override separator for empty lists to prevent trailing-comma artifacts
|
|
103
|
+
@if list.length($list) == 0 {
|
|
104
|
+
$separator: space;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@return list.append($list, $val, $separator: $separator);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Return the sublist of $list from $start to $end (1-based, inclusive)
|
|
111
|
+
/// @param {List | ArgList} $list - The list to slice
|
|
112
|
+
/// @param {Number | Null} $start [null] - Start index; defaults to 1
|
|
113
|
+
/// @param {Number | Null} $end [null] - End index; defaults to list length
|
|
114
|
+
/// @return {List} Extracted sublist
|
|
115
|
+
@function slice($list, $start: null, $end: null) {
|
|
116
|
+
@if not list.index(('list', 'arglist'), meta.type-of($list)) {
|
|
117
|
+
@error '$list: #{meta.inspect($list)} is not a list or arglist.';
|
|
118
|
+
} @else if $start != null and meta.type-of($start) != 'number' {
|
|
119
|
+
@error '$start: #{meta.inspect($start)} is not a number or null.';
|
|
120
|
+
} @else if $end != null and meta.type-of($end) != 'number' {
|
|
121
|
+
@error '$end: #{meta.inspect($end)} is not a number or null.';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
$length: list.length($list);
|
|
125
|
+
|
|
126
|
+
// Default to full-list bounds when not specified
|
|
127
|
+
@if $start == null {
|
|
128
|
+
$start: 1;
|
|
129
|
+
}
|
|
130
|
+
@if $end == null {
|
|
131
|
+
$end: $length;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Clamp bounds to the valid index range of the list
|
|
135
|
+
$start: math.round(math.max(1, math.min($start, $length)));
|
|
136
|
+
@if $end < 1 {
|
|
137
|
+
$end: 0;
|
|
138
|
+
} @else {
|
|
139
|
+
$end: math.round(math.min($end, $length));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Inverted range means no elements to extract
|
|
143
|
+
@if $end < $start {
|
|
144
|
+
@return ();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$result: ();
|
|
148
|
+
@for $i from $start through $end {
|
|
149
|
+
$result: push($result, list.nth($list, $i));
|
|
150
|
+
}
|
|
151
|
+
@return $result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Sort $list alphabetically — case-insensitive. Numbers sort by value.
|
|
155
|
+
/// @param {List | ArgList} $list - The list to sort
|
|
156
|
+
/// @param {Bool} $desc [false] - Pass `true` for descending order
|
|
157
|
+
/// @return {List} Sorted list
|
|
158
|
+
@function sort($list, $desc: false) {
|
|
159
|
+
@if not list.index(('list', 'arglist'), meta.type-of($list)) {
|
|
160
|
+
@error '$list: #{meta.inspect($list)} is not a list or arglist.';
|
|
161
|
+
} @else if meta.type-of($desc) != 'bool' {
|
|
162
|
+
@error '$desc: #{meta.inspect($desc)} is not a bool.';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$n: list.length($list);
|
|
166
|
+
@if $n <= 1 {
|
|
167
|
+
@return $list;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Character weight map — unknown chars (e.g. digits) fall back to 999 and sort to the end
|
|
171
|
+
$chars: 'abcdefghijklmnopqrstuvwxyz-';
|
|
172
|
+
|
|
173
|
+
// Middle-element pivot minimises worst-case depth on already-sorted input
|
|
174
|
+
$pivot: list.nth($list, math.ceil(math.div($n, 2)));
|
|
175
|
+
$pivot-lower: string.to-lower-case('#{$pivot}'); // Hoisted — shared by every item comparison
|
|
176
|
+
|
|
177
|
+
// 3-way partition buckets
|
|
178
|
+
$less: ();
|
|
179
|
+
$equal: ();
|
|
180
|
+
$greater: ();
|
|
181
|
+
|
|
182
|
+
// Partition: each item lands in exactly one bucket relative to the pivot
|
|
183
|
+
@each $item in $list {
|
|
184
|
+
@if $item == $pivot {
|
|
185
|
+
$equal: list.append($equal, $item);
|
|
186
|
+
} @else {
|
|
187
|
+
$is-less: null; // null = no decision yet
|
|
188
|
+
|
|
189
|
+
// Numbers compare by value — string coercion would give wrong order (e.g. '10' < '9')
|
|
190
|
+
@if meta.type-of($item) == 'number' and meta.type-of($pivot) == 'number' {
|
|
191
|
+
$is-less: $item < $pivot;
|
|
192
|
+
} @else {
|
|
193
|
+
// Walk characters until a difference is found, then exit immediately
|
|
194
|
+
$a: string.to-lower-case('#{$item}');
|
|
195
|
+
$b: $pivot-lower;
|
|
196
|
+
$len-a: string.length($a);
|
|
197
|
+
$len-b: string.length($b);
|
|
198
|
+
$min-len: math.min($len-a, $len-b);
|
|
199
|
+
$i: 1;
|
|
200
|
+
|
|
201
|
+
@while $i <= $min-len and $is-less == null {
|
|
202
|
+
$ca: string.slice($a, $i, $i);
|
|
203
|
+
$cb: string.slice($b, $i, $i);
|
|
204
|
+
@if $ca != $cb {
|
|
205
|
+
$is-less: (string.index($chars, $ca) or 999) < (string.index($chars, $cb) or 999);
|
|
206
|
+
}
|
|
207
|
+
$i: $i + 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Equal prefix — shorter string sorts first
|
|
211
|
+
@if $is-less == null {
|
|
212
|
+
$is-less: $len-a < $len-b;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@if $is-less {
|
|
217
|
+
$less: list.append($less, $item);
|
|
218
|
+
} @else {
|
|
219
|
+
$greater: list.append($greater, $item);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Recurse on each partition, then join — flip less/greater order for descending
|
|
225
|
+
@if $desc {
|
|
226
|
+
@return list.join(list.join(sort($greater, $desc), $equal), sort($less, $desc));
|
|
227
|
+
}
|
|
228
|
+
@return list.join(list.join(sort($less, $desc), $equal), sort($greater, $desc));
|
|
229
|
+
}
|
package/meta.scss
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
@use 'sass:meta';
|
|
2
|
+
@use 'sass:list';
|
|
3
|
+
@use 'sass:map';
|
|
4
|
+
@use './string' as s;
|
|
5
|
+
|
|
6
|
+
// PUBLIC API ──────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/// Return the Sass type name of $value
|
|
9
|
+
/// @param {*} $value - Any Sass value
|
|
10
|
+
/// @return {String} Type name, e.g. `'number'`, `'string'`, `'map'`, `'list'`
|
|
11
|
+
@function type-of($value) {
|
|
12
|
+
@return meta.type-of($value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Check whether $value's type is one of the types in $types
|
|
16
|
+
/// @param {*} $value - The value to check
|
|
17
|
+
/// @param {List} $types - Allowed type names to match against
|
|
18
|
+
/// @return {Bool} `true` if $value's type is in $types, `false` otherwise
|
|
19
|
+
@function is-type($value, $types) {
|
|
20
|
+
@return list.index($types, meta.type-of($value)) != null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Check whether $value is empty — pass $depth > 0 to also inspect nested structures
|
|
24
|
+
/// @param {*} $value - The value to check
|
|
25
|
+
/// @param {Number | Null} $depth [0] - Recursion depth; 0 = shallow only, null = unlimited
|
|
26
|
+
/// @return {Bool} `true` if empty at the given depth, `false` otherwise
|
|
27
|
+
@function is-empty($value, $depth: 0) {
|
|
28
|
+
@if $depth != null and meta.type-of($depth) != 'number' {
|
|
29
|
+
@error '$depth: #{meta.inspect($depth)} is not a number or null.';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$type: meta.type-of($value);
|
|
33
|
+
|
|
34
|
+
// Shallow emptiness — null, blank strings, and zero-length collections
|
|
35
|
+
@if $value == null {
|
|
36
|
+
@return true;
|
|
37
|
+
} @else if $type == 'string' and s.trim($value) == '' {
|
|
38
|
+
@return true;
|
|
39
|
+
} @else if list.index(('list', 'arglist'), $type) and list.length($value) == 0 {
|
|
40
|
+
@return true;
|
|
41
|
+
} @else if $type == 'map' and list.length(map.keys($value)) == 0 {
|
|
42
|
+
@return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// No recursion requested — value passed shallow checks, treat as non-empty
|
|
46
|
+
@if $depth == 0 {
|
|
47
|
+
@return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Decrement depth for the next level, or carry null for unlimited recursion
|
|
51
|
+
$next-depth: null;
|
|
52
|
+
@if $depth != null {
|
|
53
|
+
$next-depth: $depth - 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@if list.index(('list', 'arglist'), $type) {
|
|
57
|
+
// Recurse into list and arglist elements
|
|
58
|
+
@each $element in $value {
|
|
59
|
+
@if is-empty($element, $next-depth) {
|
|
60
|
+
@return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} @else if $type == 'map' {
|
|
64
|
+
// Recurse into map keys and values
|
|
65
|
+
@each $k, $v in $value {
|
|
66
|
+
@if is-empty($k, $next-depth) or is-empty($v, $next-depth) {
|
|
67
|
+
@return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
@return false;
|
|
72
|
+
}
|
package/number.scss
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
@use 'sass:meta';
|
|
2
|
+
@use 'sass:math';
|
|
3
|
+
@use 'sass:map';
|
|
4
|
+
|
|
5
|
+
// CONFIG ──────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
// Pixel equivalents for each supported CSS length unit (used as conversion pivot)
|
|
8
|
+
$_LENGTH_UNIT_PX_RATIOS: (
|
|
9
|
+
'px': 1,
|
|
10
|
+
'rem': 16,
|
|
11
|
+
'em': 16,
|
|
12
|
+
'cm': math.div(96, 2.54),
|
|
13
|
+
'mm': math.div(96, 25.4),
|
|
14
|
+
'Q': math.div(96, 101.6),
|
|
15
|
+
'in': 96,
|
|
16
|
+
'pc': math.div(96, 6),
|
|
17
|
+
'pt': math.div(96, 72),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// DERIVED ─────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
// Expose supported units publicly
|
|
23
|
+
$SUPPORTED_LENGTH_UNITS: map.keys($_LENGTH_UNIT_PX_RATIOS);
|
|
24
|
+
|
|
25
|
+
// PUBLIC API ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/// Attach a CSS length unit to a unitless number
|
|
28
|
+
/// @param {Number} $number - A unitless number
|
|
29
|
+
/// @param {String} $unit - Target unit; see `$SUPPORTED_LENGTH_UNITS`
|
|
30
|
+
/// @return {Number} Number with the specified unit attached
|
|
31
|
+
@function add-unit($number, $unit) {
|
|
32
|
+
@if meta.type-of($number) != 'number' {
|
|
33
|
+
@error '$number: #{meta.inspect($number)} is not a number.';
|
|
34
|
+
} @else if meta.type-of($unit) != 'string' {
|
|
35
|
+
@error '$unit: #{meta.inspect($unit)} is not a string.';
|
|
36
|
+
} @else if not math.is-unitless($number) {
|
|
37
|
+
@error '$number: #{meta.inspect($number)} is not a unitless number.';
|
|
38
|
+
} @else if not map.has-key($_LENGTH_UNIT_PX_RATIOS, $unit) {
|
|
39
|
+
@error '$unit: Must be "px", "rem", "em", "cm", "mm", "Q", "in", "pc", or "pt".';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@if $unit == 'px' {
|
|
43
|
+
@return $number * 1px;
|
|
44
|
+
} @else if $unit == 'rem' {
|
|
45
|
+
@return $number * 1rem;
|
|
46
|
+
} @else if $unit == 'em' {
|
|
47
|
+
@return $number * 1em;
|
|
48
|
+
} @else if $unit == 'cm' {
|
|
49
|
+
@return $number * 1cm;
|
|
50
|
+
} @else if $unit == 'mm' {
|
|
51
|
+
@return $number * 1mm;
|
|
52
|
+
} @else if $unit == 'Q' {
|
|
53
|
+
@return $number * 1Q;
|
|
54
|
+
} @else if $unit == 'in' {
|
|
55
|
+
@return $number * 1in;
|
|
56
|
+
} @else if $unit == 'pc' {
|
|
57
|
+
@return $number * 1pc;
|
|
58
|
+
} @else if $unit == 'pt' {
|
|
59
|
+
@return $number * 1pt;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Convert a number from one CSS length unit to another
|
|
64
|
+
/// @param {Number} $number - Unitless number, or a number whose unit matches $from-unit
|
|
65
|
+
/// @param {String} $from-unit - Source unit; see `$SUPPORTED_LENGTH_UNITS`
|
|
66
|
+
/// @param {String} $to-unit - Target unit; see `$SUPPORTED_LENGTH_UNITS`
|
|
67
|
+
/// @return {Number} Converted number with the target unit attached
|
|
68
|
+
@function convert-unit($number, $from-unit, $to-unit) {
|
|
69
|
+
@if meta.type-of($number) != 'number' {
|
|
70
|
+
@error '$number: #{meta.inspect($number)} is not a number.';
|
|
71
|
+
} @else if meta.type-of($from-unit) != 'string' {
|
|
72
|
+
@error '$from-unit: #{meta.inspect($from-unit)} is not a string.';
|
|
73
|
+
} @else if meta.type-of($to-unit) != 'string' {
|
|
74
|
+
@error '$to-unit: #{meta.inspect($to-unit)} is not a string.';
|
|
75
|
+
} @else if not map.has-key($_LENGTH_UNIT_PX_RATIOS, $from-unit) {
|
|
76
|
+
@error '$from-unit: Must be "px", "rem", "em", "cm", "mm", "Q", "in", "pc", or "pt".';
|
|
77
|
+
} @else if not map.has-key($_LENGTH_UNIT_PX_RATIOS, $to-unit) {
|
|
78
|
+
@error '$to-unit: Must be "px", "rem", "em", "cm", "mm", "Q", "in", "pc", or "pt".';
|
|
79
|
+
} @else if not math.is-unitless($number) and math.unit($number) != $from-unit {
|
|
80
|
+
@error '$number: Must be unitless or use unit "#{$from-unit}".';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@if $from-unit == $to-unit {
|
|
84
|
+
@return $number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$n: math.div($number, $number * 0 + 1); // strip unit
|
|
88
|
+
$result: math.div(
|
|
89
|
+
$n * map.get($_LENGTH_UNIT_PX_RATIOS, $from-unit),
|
|
90
|
+
map.get($_LENGTH_UNIT_PX_RATIOS, $to-unit)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
@if $to-unit == 'px' {
|
|
94
|
+
@return $result * 1px;
|
|
95
|
+
} @else if $to-unit == 'rem' {
|
|
96
|
+
@return $result * 1rem;
|
|
97
|
+
} @else if $to-unit == 'em' {
|
|
98
|
+
@return $result * 1em;
|
|
99
|
+
} @else if $to-unit == 'cm' {
|
|
100
|
+
@return $result * 1cm;
|
|
101
|
+
} @else if $to-unit == 'mm' {
|
|
102
|
+
@return $result * 1mm;
|
|
103
|
+
} @else if $to-unit == 'Q' {
|
|
104
|
+
@return $result * 1Q;
|
|
105
|
+
} @else if $to-unit == 'in' {
|
|
106
|
+
@return $result * 1in;
|
|
107
|
+
} @else if $to-unit == 'pc' {
|
|
108
|
+
@return $result * 1pc;
|
|
109
|
+
} @else if $to-unit == 'pt' {
|
|
110
|
+
@return $result * 1pt;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Resolve a relative index (positive or negative) to a valid 1-based index
|
|
115
|
+
/// @param {Number} $index - Positive or negative integer index
|
|
116
|
+
/// @param {Number} $length - Length of the collection
|
|
117
|
+
/// @return {Number | Null} 1-based index, or `null` if out of bounds
|
|
118
|
+
@function resolve-index($index, $length) {
|
|
119
|
+
@if meta.type-of($index) != 'number' {
|
|
120
|
+
@error '$index: #{meta.inspect($index)} is not a number.';
|
|
121
|
+
} @else if meta.type-of($length) != 'number' {
|
|
122
|
+
@error '$length: #{meta.inspect($length)} is not a number.';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Snap fractional inputs to the nearest whole position
|
|
126
|
+
$index: math.round($index);
|
|
127
|
+
|
|
128
|
+
// Translate negative indices from the end of the range
|
|
129
|
+
@if $index < 0 {
|
|
130
|
+
$index: $length + $index + 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Signal out-of-bounds by returning null rather than throwing
|
|
134
|
+
@if $index < 1 or $index > $length {
|
|
135
|
+
@return null;
|
|
136
|
+
} @else {
|
|
137
|
+
@return $index;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Round a number to a fixed number of decimal places
|
|
142
|
+
/// @param {Number} $number - Number to round
|
|
143
|
+
/// @param {Number} $digits [0] - Decimal places to keep (>= 0); fractional values are floored
|
|
144
|
+
/// @return {Number} Rounded number
|
|
145
|
+
@function to-fixed($number, $digits: 0) {
|
|
146
|
+
@if meta.type-of($number) != 'number' {
|
|
147
|
+
@error '$number: #{meta.inspect($number)} is not a number.';
|
|
148
|
+
} @else if meta.type-of($digits) != 'number' {
|
|
149
|
+
@error '$digits: #{meta.inspect($digits)} is not a number.';
|
|
150
|
+
} @else if not math.is-unitless($digits) {
|
|
151
|
+
@error '$digits: #{meta.inspect($digits)} is not a unitless number.';
|
|
152
|
+
} @else if $digits < 0 {
|
|
153
|
+
@error '$digits: Must be 0 or greater.';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$digits: math.floor($digits);
|
|
157
|
+
$factor: math.pow(10, $digits);
|
|
158
|
+
@return math.div(math.round($number * $factor), $factor);
|
|
159
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sass-func",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A collection of Sass utility functions for lists, strings, numbers, and meta — inspired by familiar JS APIs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sass",
|
|
7
|
+
"scss",
|
|
8
|
+
"functions",
|
|
9
|
+
"utilities",
|
|
10
|
+
"helpers",
|
|
11
|
+
"list",
|
|
12
|
+
"string",
|
|
13
|
+
"number",
|
|
14
|
+
"meta"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/nicholasgillespie/sass-func#readme",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Nicholas Gillespie (https://github.com/nicholasgillespie)",
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "github",
|
|
21
|
+
"url": "https://github.com/sponsors/nicholasgillespie"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"list.scss",
|
|
25
|
+
"meta.scss",
|
|
26
|
+
"number.scss",
|
|
27
|
+
"string.scss",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/nicholasgillespie/sass-func.git"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"sass": ">=1.33.0",
|
|
36
|
+
"sass-embedded": ">=1.33.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"sass": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"sass-embedded": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/string.scss
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
@use 'sass:meta';
|
|
2
|
+
@use 'sass:string';
|
|
3
|
+
@use 'sass:list';
|
|
4
|
+
|
|
5
|
+
// PUBLIC API ──────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/// Capitalize first letter of $string and lowercase the remainder
|
|
8
|
+
/// @param {String} $string - Input string
|
|
9
|
+
/// @return {String} Capitalized string
|
|
10
|
+
@function capitalize($string) {
|
|
11
|
+
@if meta.type-of($string) != 'string' {
|
|
12
|
+
@error '$string: #{meta.inspect($string)} is not a string.';
|
|
13
|
+
}
|
|
14
|
+
$string: trim($string);
|
|
15
|
+
@if string.length($string) == 0 {
|
|
16
|
+
@return '';
|
|
17
|
+
}
|
|
18
|
+
$first: string.slice($string, 1, 1);
|
|
19
|
+
$rest: string.slice($string, 2);
|
|
20
|
+
@return string.to-upper-case($first) + string.to-lower-case($rest);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Replace all occurrences of $pattern with $replacement in $string
|
|
24
|
+
/// @param {String} $string - Input string
|
|
25
|
+
/// @param {String} $pattern - Substring to find
|
|
26
|
+
/// @param {String} $replacement [''] - Replacement string
|
|
27
|
+
/// @return {String} String with all $pattern occurrences replaced
|
|
28
|
+
@function replace-all($string, $pattern, $replacement: '') {
|
|
29
|
+
@if meta.type-of($string) != 'string' {
|
|
30
|
+
@error '$string: #{meta.inspect($string)} is not a string.';
|
|
31
|
+
} @else if meta.type-of($pattern) != 'string' {
|
|
32
|
+
@error '$pattern: #{meta.inspect($pattern)} is not a string.';
|
|
33
|
+
} @else if meta.type-of($replacement) != 'string' {
|
|
34
|
+
@error '$replacement: #{meta.inspect($replacement)} is not a string.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Empty pattern would cause infinite recursion — return early
|
|
38
|
+
@if $pattern == '' {
|
|
39
|
+
@return $string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
$index: string.index($string, $pattern);
|
|
43
|
+
@if $index {
|
|
44
|
+
@return string.slice($string, 1, $index - 1) + $replacement +
|
|
45
|
+
replace-all(string.slice($string, $index + string.length($pattern)), $pattern, $replacement);
|
|
46
|
+
}
|
|
47
|
+
@return $string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Convert $string to URL-safe slug format (lowercase, hyphens, no special chars)
|
|
51
|
+
/// @param {String} $string - Input string
|
|
52
|
+
/// @return {String} URL-safe slug
|
|
53
|
+
@function slugify($string) {
|
|
54
|
+
@if meta.type-of($string) != 'string' {
|
|
55
|
+
@error '$string: #{meta.inspect($string)} is not a string.';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$lower: string.to-lower-case($string);
|
|
59
|
+
$result: '';
|
|
60
|
+
$disallowed-characters: '\'"!@#$%^&*()+=[]{}|;:,.<>?/`~';
|
|
61
|
+
|
|
62
|
+
// Walk each character, keeping only allowed ones
|
|
63
|
+
@for $i from 1 through string.length($lower) {
|
|
64
|
+
$character: string.slice($lower, $i, $i);
|
|
65
|
+
@if not string.index($disallowed-characters, $character) {
|
|
66
|
+
$result: $result + $character;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@return replace-all(trim($result), ' ', '-');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Split $string into a list using $separator
|
|
74
|
+
/// @param {String} $string - Input string
|
|
75
|
+
/// @param {String | Null} $separator [null] - Delimiter; '' splits per char, null wraps in a list
|
|
76
|
+
/// @return {List} List of string segments
|
|
77
|
+
@function split($string, $separator: null) {
|
|
78
|
+
@if meta.type-of($string) != 'string' {
|
|
79
|
+
@error '$string: #{meta.inspect($string)} is not a string.';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Null separator — return the string wrapped in a single-element list
|
|
83
|
+
@if $separator == null {
|
|
84
|
+
@return list.append((), $string, space);
|
|
85
|
+
}
|
|
86
|
+
@if meta.type-of($separator) != 'string' {
|
|
87
|
+
@error '$separator: #{meta.inspect($separator)} is not a string or null.';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$result: ();
|
|
91
|
+
|
|
92
|
+
// Empty separator — split into individual characters
|
|
93
|
+
@if $separator == '' {
|
|
94
|
+
@for $i from 1 through string.length($string) {
|
|
95
|
+
$result: list.append($result, string.slice($string, $i, $i), comma);
|
|
96
|
+
}
|
|
97
|
+
@return $result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$sep-length: string.length($separator);
|
|
101
|
+
$index: string.index($string, $separator);
|
|
102
|
+
|
|
103
|
+
// Iteratively slice at each separator match, accumulating parts
|
|
104
|
+
@while $index {
|
|
105
|
+
$part: string.slice($string, 1, $index - 1);
|
|
106
|
+
$result: list.append($result, $part, comma);
|
|
107
|
+
$string: string.slice($string, $index + $sep-length);
|
|
108
|
+
$index: string.index($string, $separator);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@return list.append($result, $string, comma);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Remove leading and trailing whitespace from $string
|
|
115
|
+
/// @param {String} $string - Input string
|
|
116
|
+
/// @return {String} Trimmed string
|
|
117
|
+
@function trim($string) {
|
|
118
|
+
@if meta.type-of($string) != 'string' {
|
|
119
|
+
@error '$string: #{meta.inspect($string)} is not a string.';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$start: 1;
|
|
123
|
+
$end: string.length($string);
|
|
124
|
+
@while $start <= $end and string.slice($string, $start, $start) == ' ' {
|
|
125
|
+
$start: $start + 1;
|
|
126
|
+
}
|
|
127
|
+
@while $end >= $start and string.slice($string, $end, $end) == ' ' {
|
|
128
|
+
$end: $end - 1;
|
|
129
|
+
}
|
|
130
|
+
@return string.slice($string, $start, $end);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Convert $string to UPPER_SNAKE_CASE by replacing hyphens and spaces with underscores
|
|
134
|
+
/// @param {String} $string - Input string
|
|
135
|
+
/// @return {String} UPPER_SNAKE_CASE string
|
|
136
|
+
@function upper-snake-case($string) {
|
|
137
|
+
@if meta.type-of($string) != 'string' {
|
|
138
|
+
@error '$string: #{meta.inspect($string)} is not a string.';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$string: trim($string);
|
|
142
|
+
$result: replace-all($string, '-', '_');
|
|
143
|
+
$result: replace-all($result, ' ', '_');
|
|
144
|
+
@return string.to-upper-case($result);
|
|
145
|
+
}
|