|
1 |
| -const UNDERSCORE_UNICODE = 95; |
2 |
| - |
3 | 1 | /**
|
4 | 2 | * This length includes the underscore,
|
5 | 3 | * e.g. `"_1s4A"` would be a valid atomic group hash.
|
6 | 4 | */
|
7 | 5 | const ATOMIC_GROUP_LENGTH = 5;
|
8 | 6 |
|
9 | 7 | /**
|
10 |
| - * Joins classes together and ensures atomic declarations of a single group exist. |
11 |
| - * Atomic declarations take the form of `_{group}{value}` (always prefixed with an underscore), |
12 |
| - * where both `group` and `value` are hashes **four characters long**. |
13 |
| - * Class names can be of any length, |
14 |
| - * this function can take both atomic declarations and class names. |
15 |
| - * |
16 |
| - * Input: |
| 8 | + * Create a single string containing all the classnames provided, separated by a space (`" "`). |
| 9 | + * The result will only contain the _last_ atomic style classname for each atomic `group`. |
17 | 10 | *
|
18 |
| - * ``` |
19 |
| - * ax(['_aaaabbbb', '_aaaacccc']) |
| 11 | + * ```ts |
| 12 | + * ax(['_aaaabbbb', '_aaaacccc']); |
| 13 | + * // output |
| 14 | + * '_aaaacccc' |
20 | 15 | * ```
|
21 | 16 | *
|
22 |
| - * Output: |
| 17 | + * Format of Atomic style classnames: `_{group}{value}` (`_\w{4}\w{4}`) |
23 | 18 | *
|
24 |
| - * ``` |
25 |
| - * '_aaaacccc' |
26 |
| - * ``` |
| 19 | + * `ax` will preserve any non atomic style classnames (eg `"border-red"`) |
27 | 20 | *
|
28 |
| - * @param classes |
| 21 | + * ```ts |
| 22 | + * ax(['_aaaabbbb', '_aaaacccc', 'border-red']); |
| 23 | + * // output |
| 24 | + * '_aaaacccc border-red' |
| 25 | + * ``` |
29 | 26 | */
|
30 | 27 | export default function ax(classNames: (string | undefined | null | false)[]): string | undefined {
|
31 |
| - if (classNames.length <= 1 && (!classNames[0] || classNames[0].indexOf(' ') === -1)) { |
32 |
| - // short circuit if there's no custom class names. |
33 |
| - return classNames[0] || undefined; |
| 28 | + // Shortcut: nothing to do |
| 29 | + if (!classNames.length) { |
| 30 | + return; |
34 | 31 | }
|
35 | 32 |
|
36 |
| - const atomicGroups: Record<string, string> = {}; |
| 33 | + // Shortcut: don't need to do anything if we only have a single classname |
| 34 | + if ( |
| 35 | + classNames.length === 1 && |
| 36 | + classNames[0] && |
| 37 | + // checking to see if `classNames[0]` is a string that contains other classnames |
| 38 | + !classNames[0].includes(' ') |
| 39 | + ) { |
| 40 | + return classNames[0]; |
| 41 | + } |
37 | 42 |
|
38 |
| - for (let i = 0; i < classNames.length; i++) { |
39 |
| - const cls = classNames[i]; |
40 |
| - if (!cls) { |
| 43 | + // Using an object rather than a `Map` as it performed better in our benchmarks. |
| 44 | + // Would be happy to move to `Map` if it proved to be better under real conditions. |
| 45 | + const map: Record<string, string> = {}; |
| 46 | + |
| 47 | + // Note: using loops to minimize iterations over the collection |
| 48 | + for (const value of classNames) { |
| 49 | + // Exclude all falsy values, which leaves us with populated strings |
| 50 | + if (!value) { |
41 | 51 | continue;
|
42 | 52 | }
|
43 | 53 |
|
44 |
| - const groups = cls.split(' '); |
| 54 | + // a `value` can contain multiple classnames |
| 55 | + const list = value.split(' '); |
45 | 56 |
|
46 |
| - for (let x = 0; x < groups.length; x++) { |
47 |
| - const atomic = groups[x]; |
48 |
| - const atomicGroupName = atomic.slice( |
49 |
| - 0, |
50 |
| - atomic.charCodeAt(0) === UNDERSCORE_UNICODE ? ATOMIC_GROUP_LENGTH : undefined |
51 |
| - ); |
52 |
| - atomicGroups[atomicGroupName] = atomic; |
| 57 | + for (const className of list) { |
| 58 | + /** |
| 59 | + * For atomic style classnames: the `key` is the `group` |
| 60 | + * |
| 61 | + * - Later atomic classnames with the same `group` will override earlier ones |
| 62 | + * (which is what we want). |
| 63 | + * - Assumes atomic classnames are the only things that start with `_` |
| 64 | + * - Could use a regex to ensure that atomic classnames are structured how we expect, |
| 65 | + * but did not add that for now as it did slow things down a bit. |
| 66 | + * |
| 67 | + * For other classnames: the `key` is the whole classname |
| 68 | + * - Okay to remove duplicates as doing so does not impact specificity |
| 69 | + * |
| 70 | + * */ |
| 71 | + const key = className.startsWith('_') ? className.slice(0, ATOMIC_GROUP_LENGTH) : className; |
| 72 | + map[key] = className; |
53 | 73 | }
|
54 | 74 | }
|
55 | 75 |
|
56 |
| - let str = ''; |
| 76 | + /** |
| 77 | + * We are converting the `map` into a string. |
| 78 | + * |
| 79 | + * The simple way to do this would be `Object.values(map).join(' ')`. |
| 80 | + * However, the approach below performs 10%-20% better in benchmarks. |
| 81 | + * |
| 82 | + * For `ax()` it feels right to squeeze as much runtime performance out as we can. |
| 83 | + */ |
| 84 | + let result: string = ''; |
| 85 | + for (const key in map) { |
| 86 | + result += map[key] + ' '; |
| 87 | + } |
57 | 88 |
|
58 |
| - for (const key in atomicGroups) { |
59 |
| - const value = atomicGroups[key]; |
60 |
| - str += value + ' '; |
| 89 | + // If we have an empty string, then our `map` was empty. |
| 90 | + if (!result) { |
| 91 | + return; |
61 | 92 | }
|
62 | 93 |
|
63 |
| - return str.slice(0, -1); |
| 94 | + // remove last " " from the result (we added " " at the end of every value) |
| 95 | + return result.trimEnd(); |
64 | 96 | }
|
0 commit comments