clsx-ruby 1.1.2 → 1.1.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +11 -10
- data/lib/clsx/helper.rb +227 -69
- data/lib/clsx/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7af7dbc1bf63dae86a10afc293087f0528b82d479e0f4cc2e82a6944c79c8ed4
|
|
4
|
+
data.tar.gz: 2ed149e30e3da918e403697ea511ca597c6bece1bbb2965ae6dd02e0d054a4b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37be556d78cb762df769303b6143e7872fd0bc67f26110720e31cf34238d3c657cab2f09b39f899163babcd7086c044d281d157261cb73736e7fcf4fe81d6b05
|
|
7
|
+
data.tar.gz: 6c5af47dba54f7d17fa5ba00a8f9bd2e96f34581cdd3f3544702b93699ec9bb0f5068147e294b5cff976341f64aa56353f2713b90929dbdfd443a5d83a62560e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## v1.1.3
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Normalize whitespace in class strings: tabs, newlines, leading/trailing spaces, and consecutive spaces are collapsed to single spaces (e.g., `clsx("foo\tbar")` returns `"foo bar"`)
|
|
15
|
+
- Whitespace-only strings now correctly return `nil` (e.g., `clsx(" ")` returns `nil`)
|
|
16
|
+
- Deduplicate class names across multi-token strings (e.g., `clsx("foo bar", "foo")` now returns `"foo bar"`)
|
|
17
|
+
|
|
10
18
|
## v1.1.2
|
|
11
19
|
|
|
12
20
|
### Changed
|
data/README.md
CHANGED
|
@@ -34,16 +34,16 @@ For Rails integration (adds `clsx` and `cn` helpers to all views), see [clsx-rai
|
|
|
34
34
|
|
|
35
35
|
### Blazing fast
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**2-3x faster** than Rails `class_names` across every scenario:
|
|
38
38
|
|
|
39
39
|
| Scenario | clsx-ruby | Rails `class_names` | Speedup |
|
|
40
40
|
|---|---|---|---|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
| Hash |
|
|
46
|
-
|
|
|
41
|
+
| String array | 1.3M i/s | 369K i/s | **3.4x** |
|
|
42
|
+
| Multiple strings | 1.3M i/s | 408K i/s | **3.3x** |
|
|
43
|
+
| Single string | 2.4M i/s | 893K i/s | **2.7x** |
|
|
44
|
+
| Mixed types | 912K i/s | 358K i/s | **2.5x** |
|
|
45
|
+
| Hash | 1.7M i/s | 672K i/s | **2.5x** |
|
|
46
|
+
| String + hash | 1.2M i/s | 565K i/s | **2.1x** |
|
|
47
47
|
|
|
48
48
|
<sup>Ruby 4.0.1, Apple M1 Pro. Reproduce: `bundle exec ruby benchmark/run.rb`</sup>
|
|
49
49
|
|
|
@@ -53,7 +53,7 @@ For Rails integration (adds `clsx` and `cn` helpers to all views), see [clsx-rai
|
|
|
53
53
|
|---|---|---|
|
|
54
54
|
| Conditional classes | ✅ | ✅ |
|
|
55
55
|
| Auto-deduplication | ✅ | ✅ |
|
|
56
|
-
| 3
|
|
56
|
+
| 2–3× faster | ✅ | ❌ |
|
|
57
57
|
| Returns `nil` when empty | ✅ | ❌ (returns `""`) |
|
|
58
58
|
| Complex hash keys | ✅ | ❌ |
|
|
59
59
|
| Framework-agnostic | ✅ | ❌ |
|
|
@@ -228,9 +228,10 @@ end
|
|
|
228
228
|
Clsx[nil, false] # => nil
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
2. **Deduplication** — Duplicate classes are automatically removed:
|
|
231
|
+
2. **Deduplication** — Duplicate classes are automatically removed, even across multi-token strings:
|
|
232
232
|
```ruby
|
|
233
|
-
Clsx['foo', 'foo']
|
|
233
|
+
Clsx['foo', 'foo'] # => 'foo'
|
|
234
|
+
Clsx['foo bar', 'foo'] # => 'foo bar'
|
|
234
235
|
```
|
|
235
236
|
|
|
236
237
|
3. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
|
data/lib/clsx/helper.rb
CHANGED
|
@@ -26,12 +26,18 @@ module Clsx
|
|
|
26
26
|
# clsx(%w[foo bar], hidden: true) # => "foo bar hidden"
|
|
27
27
|
def clsx(*args)
|
|
28
28
|
return nil if args.empty?
|
|
29
|
-
return
|
|
30
|
-
|
|
29
|
+
return clsx_one(args[0]) if args.size == 1
|
|
30
|
+
|
|
31
|
+
if args.size == 2 && args[0].is_a?(String) && args[1].is_a?(Hash)
|
|
32
|
+
str = args[0]
|
|
33
|
+
return clsx_hash(args[1]) if str.empty?
|
|
34
|
+
return clsx_str_hash_full(str, args[1]) if str.include?(' ') || str.include?("\t") || str.include?("\n") # rubocop:disable Layout/EmptyLineAfterGuardClause
|
|
35
|
+
return clsx_str_hash(str, args[1])
|
|
36
|
+
end
|
|
31
37
|
|
|
32
38
|
seen = {}
|
|
33
|
-
|
|
34
|
-
seen
|
|
39
|
+
clsx_walk(args, seen)
|
|
40
|
+
clsx_join(seen)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
# (see #clsx)
|
|
@@ -39,131 +45,283 @@ module Clsx
|
|
|
39
45
|
|
|
40
46
|
private
|
|
41
47
|
|
|
42
|
-
# Single-argument fast path — dispatches by type
|
|
43
|
-
# a
|
|
48
|
+
# Single-argument fast path — dispatches by type, handles multi-token
|
|
49
|
+
# string dedup without allocating a walker Hash for simple cases.
|
|
44
50
|
#
|
|
45
|
-
# @param arg [
|
|
46
|
-
# @return [String]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
# @param arg [Object] single class descriptor
|
|
52
|
+
# @return [String, nil]
|
|
53
|
+
def clsx_one(arg)
|
|
54
|
+
if arg.is_a?(String)
|
|
55
|
+
return nil if arg.empty?
|
|
56
|
+
return arg unless arg.include?(' ') || arg.include?("\t") || arg.include?("\n")
|
|
57
|
+
|
|
58
|
+
parts = arg.split
|
|
59
|
+
return nil if parts.empty?
|
|
60
|
+
return parts[0] if parts.length == 1
|
|
61
|
+
return arg if !parts.uniq! && parts.length == arg.count(' ') + 1 # rubocop:disable Layout/EmptyLineAfterGuardClause
|
|
62
|
+
return parts.join(' ')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if arg.is_a?(Symbol)
|
|
66
|
+
s = arg.name
|
|
67
|
+
return s unless s.include?(' ') || s.include?("\t") || s.include?("\n") # rubocop:disable Layout/EmptyLineAfterGuardClause
|
|
68
|
+
return clsx_dedup_str(s)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return clsx_hash(arg) if arg.is_a?(Hash)
|
|
52
72
|
|
|
53
73
|
if arg.is_a?(Array)
|
|
54
74
|
return nil if arg.empty?
|
|
55
75
|
|
|
56
|
-
if arg.all?(String)
|
|
57
|
-
seen = {}
|
|
58
|
-
arg.each { |s| seen[s] = true unless s.empty? }
|
|
59
|
-
return seen.empty? ? nil : seen.keys.join(' ')
|
|
60
|
-
end
|
|
61
|
-
|
|
62
76
|
seen = {}
|
|
63
|
-
|
|
64
|
-
return seen
|
|
77
|
+
clsx_walk(arg, seen)
|
|
78
|
+
return clsx_join(seen)
|
|
65
79
|
end
|
|
66
80
|
|
|
67
|
-
return arg.to_s if arg.is_a?(Numeric)
|
|
68
81
|
return nil if !arg || arg == true || arg.is_a?(Proc)
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
s = arg.to_s
|
|
84
|
+
s.empty? ? nil : s
|
|
72
85
|
end
|
|
73
86
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
87
|
+
# Dedup and normalize a multi-token string. Handles whitespace-only
|
|
88
|
+
# input, leading/trailing whitespace, tabs, newlines, and duplicates.
|
|
76
89
|
#
|
|
77
|
-
# @param
|
|
78
|
-
# @return [String]
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
# @param str [String] space-separated class string
|
|
91
|
+
# @return [String, nil]
|
|
92
|
+
def clsx_dedup_str(str)
|
|
93
|
+
parts = str.split
|
|
94
|
+
return nil if parts.empty?
|
|
95
|
+
return parts[0] if parts.length == 1
|
|
96
|
+
return str if !parts.uniq! && parts.length == str.count(' ') + 1
|
|
97
|
+
|
|
98
|
+
parts.join(' ')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Hash-only fast path using string buffer. Falls back to Hash dedup
|
|
102
|
+
# on mixed key types, multi-token keys, or complex keys.
|
|
103
|
+
#
|
|
104
|
+
# @param hash [Hash] class-name => condition pairs
|
|
105
|
+
# @return [String, nil]
|
|
106
|
+
def clsx_hash(hash)
|
|
81
107
|
return nil if hash.empty?
|
|
82
108
|
|
|
83
109
|
buf = nil
|
|
110
|
+
key_type = nil
|
|
111
|
+
|
|
84
112
|
hash.each do |key, value|
|
|
85
113
|
next unless value
|
|
86
114
|
|
|
87
115
|
if key.is_a?(Symbol)
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
return clsx_hash_full(hash) if key_type == :string
|
|
117
|
+
|
|
118
|
+
key_type = :symbol
|
|
119
|
+
s = key.name
|
|
120
|
+
return clsx_hash_full(hash) if s.include?(' ')
|
|
121
|
+
|
|
122
|
+
buf ? (buf << ' ' << s) : (buf = s.dup)
|
|
90
123
|
elsif key.is_a?(String)
|
|
91
124
|
next if key.empty?
|
|
92
125
|
|
|
126
|
+
return clsx_hash_full(hash) if key_type == :symbol
|
|
127
|
+
return clsx_hash_full(hash) if key.include?(' ')
|
|
128
|
+
|
|
129
|
+
key_type = :string
|
|
93
130
|
buf ? (buf << ' ' << key) : (buf = key.dup)
|
|
94
131
|
else
|
|
95
|
-
|
|
96
|
-
clsx_process([hash], seen)
|
|
97
|
-
return seen.empty? ? nil : seen.keys.join(' ')
|
|
132
|
+
return clsx_hash_full(hash)
|
|
98
133
|
end
|
|
99
134
|
end
|
|
135
|
+
|
|
136
|
+
return nil unless buf
|
|
137
|
+
return clsx_dedup_str(buf) if buf.include?("\t") || buf.include?("\n")
|
|
138
|
+
|
|
100
139
|
buf
|
|
101
140
|
end
|
|
102
141
|
|
|
103
|
-
#
|
|
104
|
-
# the base string. Falls back to {#clsx_process} on non-String/Symbol keys.
|
|
142
|
+
# Hash fallback with full dedup via walker.
|
|
105
143
|
#
|
|
106
|
-
# @param
|
|
107
|
-
# @
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
# @param hash [Hash] class-name => condition pairs
|
|
145
|
+
# @return [String, nil]
|
|
146
|
+
def clsx_hash_full(hash)
|
|
147
|
+
seen = {}
|
|
148
|
+
clsx_walk_hash(hash, seen)
|
|
149
|
+
clsx_join(seen)
|
|
150
|
+
end
|
|
112
151
|
|
|
152
|
+
# Fast path for +clsx('base', active: cond)+ pattern where base is a
|
|
153
|
+
# single token. Deduplicates via direct string comparison.
|
|
154
|
+
#
|
|
155
|
+
# @param str [String] base class name
|
|
156
|
+
# @param hash [Hash] class-name => condition pairs
|
|
157
|
+
# @return [String, nil]
|
|
158
|
+
def clsx_str_hash(str, hash)
|
|
113
159
|
buf = str.dup
|
|
160
|
+
key_type = nil
|
|
161
|
+
|
|
162
|
+
hash.each do |key, value|
|
|
163
|
+
next unless value
|
|
164
|
+
|
|
165
|
+
if key.is_a?(Symbol)
|
|
166
|
+
return clsx_str_hash_full(str, hash) if key_type == :string
|
|
167
|
+
|
|
168
|
+
key_type = :symbol
|
|
169
|
+
s = key.name
|
|
170
|
+
return clsx_str_hash_full(str, hash) if s.include?(' ')
|
|
171
|
+
|
|
172
|
+
next if s == str
|
|
173
|
+
|
|
174
|
+
buf << ' ' << s
|
|
175
|
+
elsif key.is_a?(String)
|
|
176
|
+
return clsx_str_hash_full(str, hash) if key_type == :symbol
|
|
177
|
+
|
|
178
|
+
key_type = :string
|
|
179
|
+
next if key.empty?
|
|
180
|
+
|
|
181
|
+
return clsx_str_hash_full(str, hash) if key.include?(' ')
|
|
182
|
+
|
|
183
|
+
next if key == str
|
|
184
|
+
|
|
185
|
+
buf << ' ' << key
|
|
186
|
+
else
|
|
187
|
+
return clsx_str_hash_full(str, hash)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
return clsx_dedup_str(buf) if buf.include?("\t") || buf.include?("\n")
|
|
192
|
+
|
|
193
|
+
buf
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Full str+hash dedup using array lookup. Splits the base string once,
|
|
197
|
+
# then checks hash keys against the parts array via linear search.
|
|
198
|
+
# Falls back to Hash dedup on mixed key types or complex keys.
|
|
199
|
+
#
|
|
200
|
+
# @param str [String] base class name (contains spaces)
|
|
201
|
+
# @param hash [Hash] class-name => condition pairs
|
|
202
|
+
# @return [String, nil]
|
|
203
|
+
def clsx_str_hash_full(str, hash)
|
|
204
|
+
parts = str.split
|
|
205
|
+
return clsx_hash(hash) if parts.empty?
|
|
206
|
+
return clsx_str_hash_full_walk(parts, hash) if parts.length == 1
|
|
207
|
+
|
|
208
|
+
buf = if parts.uniq! || parts.length != str.count(' ') + 1
|
|
209
|
+
parts.join(' ')
|
|
210
|
+
else
|
|
211
|
+
str.dup
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
key_type = nil
|
|
215
|
+
|
|
114
216
|
hash.each do |key, value|
|
|
115
217
|
next unless value
|
|
116
218
|
|
|
117
219
|
if key.is_a?(Symbol)
|
|
220
|
+
return clsx_str_hash_full_walk(parts, hash) if key_type == :string
|
|
221
|
+
|
|
222
|
+
key_type = :symbol
|
|
118
223
|
s = key.name
|
|
119
|
-
|
|
224
|
+
return clsx_str_hash_full_walk(parts, hash) if s.include?(' ') || s.include?("\t") || s.include?("\n")
|
|
225
|
+
|
|
226
|
+
next if parts.include?(s)
|
|
227
|
+
|
|
228
|
+
buf << ' ' << s
|
|
120
229
|
elsif key.is_a?(String)
|
|
121
|
-
|
|
230
|
+
return clsx_str_hash_full_walk(parts, hash) if key_type == :symbol
|
|
231
|
+
|
|
232
|
+
key_type = :string
|
|
233
|
+
next if key.empty?
|
|
234
|
+
return clsx_str_hash_full_walk(parts, hash) if key.include?(' ') || key.include?("\t") || key.include?("\n")
|
|
235
|
+
|
|
236
|
+
next if parts.include?(key)
|
|
237
|
+
|
|
238
|
+
buf << ' ' << key
|
|
122
239
|
else
|
|
123
|
-
|
|
124
|
-
clsx_process([hash], seen)
|
|
125
|
-
return seen.size == 1 ? str : seen.keys.join(' ')
|
|
240
|
+
return clsx_str_hash_full_walk(parts, hash)
|
|
126
241
|
end
|
|
127
242
|
end
|
|
243
|
+
|
|
128
244
|
buf
|
|
129
245
|
end
|
|
130
246
|
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
247
|
+
# Hash-based fallback for str+hash when array lookup can't handle it.
|
|
248
|
+
#
|
|
249
|
+
# @param parts [Array<String>] pre-split base tokens
|
|
250
|
+
# @param hash [Hash] class-name => condition pairs
|
|
251
|
+
# @return [String, nil]
|
|
252
|
+
def clsx_str_hash_full_walk(parts, hash)
|
|
253
|
+
seen = {}
|
|
254
|
+
parts.each { |s| seen[s] = true }
|
|
255
|
+
clsx_walk_hash(hash, seen)
|
|
256
|
+
clsx_join(seen)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# General-purpose recursive walker. Stores strings as-is; normalization
|
|
260
|
+
# and dedup are handled by {#clsx_join} after walking.
|
|
134
261
|
#
|
|
135
|
-
# @param args [Array
|
|
262
|
+
# @param args [Array] nested arguments to walk
|
|
136
263
|
# @param seen [Hash{String => true}] accumulator for deduplication
|
|
137
264
|
# @return [void]
|
|
138
|
-
def
|
|
265
|
+
def clsx_walk(args, seen)
|
|
139
266
|
args.each do |arg|
|
|
140
267
|
if arg.is_a?(String)
|
|
141
268
|
seen[arg] = true unless arg.empty?
|
|
142
269
|
elsif arg.is_a?(Symbol)
|
|
143
270
|
seen[arg.name] = true
|
|
144
271
|
elsif arg.is_a?(Array)
|
|
145
|
-
|
|
272
|
+
clsx_walk(arg, seen)
|
|
146
273
|
elsif arg.is_a?(Hash)
|
|
147
|
-
arg
|
|
148
|
-
next unless value
|
|
149
|
-
|
|
150
|
-
if key.is_a?(Symbol)
|
|
151
|
-
seen[key.name] = true
|
|
152
|
-
elsif key.is_a?(String)
|
|
153
|
-
seen[key] = true unless key.empty?
|
|
154
|
-
else
|
|
155
|
-
clsx_process([key], seen)
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
elsif arg.is_a?(Numeric)
|
|
159
|
-
seen[arg.to_s] = true
|
|
274
|
+
clsx_walk_hash(arg, seen)
|
|
160
275
|
elsif !arg || arg == true || arg.is_a?(Proc)
|
|
161
276
|
next
|
|
162
277
|
else
|
|
163
|
-
|
|
164
|
-
seen[
|
|
278
|
+
s = arg.to_s
|
|
279
|
+
seen[s] = true unless s.empty?
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Hash-specific walker — avoids wrapping hash in an array for recursion.
|
|
285
|
+
#
|
|
286
|
+
# @param hash [Hash] hash to walk
|
|
287
|
+
# @param seen [Hash{String => true}] accumulator
|
|
288
|
+
# @return [void]
|
|
289
|
+
def clsx_walk_hash(hash, seen)
|
|
290
|
+
return if hash.empty?
|
|
291
|
+
|
|
292
|
+
hash.each do |key, value|
|
|
293
|
+
next unless value
|
|
294
|
+
|
|
295
|
+
if key.is_a?(Symbol)
|
|
296
|
+
seen[key.name] = true
|
|
297
|
+
elsif key.is_a?(String)
|
|
298
|
+
seen[key] = true unless key.empty?
|
|
299
|
+
elsif key.is_a?(Array)
|
|
300
|
+
clsx_walk(key, seen)
|
|
301
|
+
elsif key.is_a?(Hash)
|
|
302
|
+
clsx_walk_hash(key, seen)
|
|
303
|
+
elsif !key || key == true || key.is_a?(Proc)
|
|
304
|
+
next
|
|
305
|
+
else
|
|
306
|
+
s = key.to_s
|
|
307
|
+
seen[s] = true unless s.empty?
|
|
165
308
|
end
|
|
166
309
|
end
|
|
167
310
|
end
|
|
311
|
+
|
|
312
|
+
# Post-join dedup and normalization: detects multi-token entries via
|
|
313
|
+
# space count mismatch or tab/newline presence, then splits and rebuilds.
|
|
314
|
+
#
|
|
315
|
+
# @param seen [Hash{String => true}] token accumulator
|
|
316
|
+
# @return [String, nil] space-joined class string
|
|
317
|
+
def clsx_join(seen)
|
|
318
|
+
return nil if seen.empty?
|
|
319
|
+
|
|
320
|
+
result = seen.keys.join(' ')
|
|
321
|
+
return result if result.count(' ') + 1 == seen.size && !result.include?("\t") && !result.include?("\n")
|
|
322
|
+
|
|
323
|
+
normalized = result.split.uniq.join(' ')
|
|
324
|
+
normalized.empty? ? nil : normalized
|
|
325
|
+
end
|
|
168
326
|
end
|
|
169
327
|
end
|
data/lib/clsx/version.rb
CHANGED