clsx-ruby 1.1.0 → 1.1.2
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 +44 -2
- data/README.md +138 -33
- data/lib/clsx/helper.rb +98 -80
- data/lib/clsx/version.rb +1 -1
- data/lib/clsx.rb +10 -10
- metadata +6 -5
- data/CLAUDE.md +0 -66
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f20a56982120e4142d5932688887cc7f4b38a3596e01ce8aacf1f7b5b15f8d4
|
|
4
|
+
data.tar.gz: ebbd199c2d2ad139c2dda52680953778092b21708e01aef7b09d4619d932333a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ed8a104aac715ac054d3399f6cf0c6e15a1b5a2df6307218c5f31172ebe7c1aca261d24b8b7ed23e3a06974002c2c1b9af7f857b4a4ebf4e90eea3694e75050
|
|
7
|
+
data.tar.gz: 83a0ea7711cab8e01b9eb8f71376af34e51d21bbbbd7da6091e1abe323d4145228c6e8ed44ec13f6421670ad1685a7857c531e198c100e553f77771d4183db45
|
data/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## v1.1.2
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Improved gemspec description and README marketing copy
|
|
15
|
+
- Switched gemspec to whitelist approach for included files
|
|
16
|
+
|
|
17
|
+
## v1.1.1
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- YARD documentation to all public and private methods
|
|
22
|
+
- Rails 8.1.2 `class_names` as a third benchmark competitor
|
|
23
|
+
- ViewComponent and Phlex examples in README
|
|
24
|
+
- Memory and profiling benchmark scripts (`benchmark/memory.rb`, `benchmark/profile.rb`, `benchmark/stackprof_run.rb`)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Replaced `class ==` type checks with idiomatic `is_a?` early returns
|
|
29
|
+
- Extracted `clsx_single` method for clearer single-argument dispatch
|
|
30
|
+
- Simplified `Cn` alias from wrapper module to `Cn = Clsx`
|
|
31
|
+
- Removed redundant `seen.key?` guards in `clsx_process`
|
|
32
|
+
- Reduced allocations in `clsx_single`: pass arrays directly to `clsx_process` instead of wrapping, handle edge types inline
|
|
33
|
+
- Moved inline rubocop disables to `.rubocop.yml` config
|
|
34
|
+
- Updated benchmark baseline to compare against previous version, not ancient one
|
|
35
|
+
- Rewrote README with benchmark numbers and feature comparison table
|
|
36
|
+
- Process hash keys inline in `clsx_process` instead of deferring to a second pass (+23% mixed, +13% complex). Hash keys now appear in declaration order, matching JS clsx behavior.
|
|
37
|
+
|
|
3
38
|
## v1.1.0
|
|
4
39
|
|
|
5
|
-
|
|
40
|
+
### Added
|
|
41
|
+
|
|
6
42
|
- New fast path for `clsx('base', active: cond)` string + hash pattern (+69%)
|
|
7
|
-
-
|
|
43
|
+
- `string + hash` benchmark scenario
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Optimized hash-only path: skip dedup Hash since hash keys are unique by definition (+8%)
|
|
8
48
|
- Use `str.dup` instead of `String.new(str)` for buffer init (+12-18% on hash paths)
|
|
9
49
|
|
|
10
50
|
## v1.0.0
|
|
11
51
|
|
|
52
|
+
### Added
|
|
53
|
+
|
|
12
54
|
- Initial release as standalone framework-agnostic gem
|
|
13
55
|
- Extracted from clsx-rails v2.0.0
|
|
14
56
|
- API: `Clsx['foo', bar: true]`, `Cn['foo', bar: true]`, `include Clsx::Helper`
|
data/README.md
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
# clsx-ruby [](https://rubygems.org/gems/clsx-ruby) [](https://github.com/svyatov/clsx-ruby/actions?query=workflow%3ACI) [](LICENSE.txt)
|
|
1
|
+
# clsx-ruby [](https://rubygems.org/gems/clsx-ruby) [](https://app.codecov.io/gh/svyatov/clsx-ruby) [](https://github.com/svyatov/clsx-ruby/actions?query=workflow%3ACI) [](LICENSE.txt)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The fastest, framework-agnostic conditional CSS class builder for Ruby.
|
|
4
|
+
> Perfect for ViewComponent, Phlex, Tailwind CSS or just standalone.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Inspired by the JavaScript [clsx](https://github.com/lukeed/clsx) package. Works with any Ruby codebase.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Requirements
|
|
10
|
-
|
|
11
|
-
Ruby 3.2+. No runtime dependencies.
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
8
|
+
## Quick Start
|
|
14
9
|
|
|
15
10
|
```bash
|
|
16
11
|
bundle add clsx-ruby
|
|
@@ -19,9 +14,55 @@ bundle add clsx-ruby
|
|
|
19
14
|
Or add it manually to the Gemfile:
|
|
20
15
|
|
|
21
16
|
```ruby
|
|
22
|
-
gem 'clsx-ruby', '~> 1.
|
|
17
|
+
gem 'clsx-ruby', '~> 1.1'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then use it:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'clsx'
|
|
24
|
+
|
|
25
|
+
Clsx['btn', 'btn-primary', active: is_active, disabled: is_disabled]
|
|
26
|
+
# => "btn btn-primary active" (when is_active is truthy, is_disabled is falsy)
|
|
23
27
|
```
|
|
24
28
|
|
|
29
|
+
## Rails Integration
|
|
30
|
+
|
|
31
|
+
For Rails integration (adds `clsx` and `cn` helpers to all views), see [clsx-rails](https://github.com/svyatov/clsx-rails).
|
|
32
|
+
|
|
33
|
+
## Why clsx-ruby?
|
|
34
|
+
|
|
35
|
+
### Blazing fast
|
|
36
|
+
|
|
37
|
+
**3-8x faster** than Rails `class_names` across every scenario:
|
|
38
|
+
|
|
39
|
+
| Scenario | clsx-ruby | Rails `class_names` | Speedup |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| Single string | 7.6M i/s | 911K i/s | **8.5x** |
|
|
42
|
+
| String + hash | 2.4M i/s | 580K i/s | **4.1x** |
|
|
43
|
+
| String array | 1.4M i/s | 357K i/s | **4.0x** |
|
|
44
|
+
| Multiple strings | 1.5M i/s | 414K i/s | **3.7x** |
|
|
45
|
+
| Hash | 2.2M i/s | 670K i/s | **3.3x** |
|
|
46
|
+
| Mixed types | 852K i/s | 367K i/s | **2.3x** |
|
|
47
|
+
|
|
48
|
+
<sup>Ruby 4.0.1, Apple M1 Pro. Reproduce: `bundle exec ruby benchmark/run.rb`</sup>
|
|
49
|
+
|
|
50
|
+
### More feature-rich than `class_names`
|
|
51
|
+
|
|
52
|
+
| Feature | clsx-ruby | Rails `class_names` |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Conditional classes | ✅ | ✅ |
|
|
55
|
+
| Auto-deduplication | ✅ | ✅ |
|
|
56
|
+
| 3–8× faster | ✅ | ❌ |
|
|
57
|
+
| Returns `nil` when empty | ✅ | ❌ (returns `""`) |
|
|
58
|
+
| Complex hash keys | ✅ | ❌ |
|
|
59
|
+
| Framework-agnostic | ✅ | ❌ |
|
|
60
|
+
| Zero dependencies | ✅ | ❌ |
|
|
61
|
+
|
|
62
|
+
### Tiny footprint
|
|
63
|
+
|
|
64
|
+
~100 lines of code. Zero runtime dependencies. Ruby 3.2+.
|
|
65
|
+
|
|
25
66
|
## Usage
|
|
26
67
|
|
|
27
68
|
### Bracket API (recommended)
|
|
@@ -93,63 +134,127 @@ Clsx[['foo', nil, false, 'bar']]
|
|
|
93
134
|
Clsx[['foo'], ['', nil, false, 'bar'], [['baz', [['hello'], 'there']]]]
|
|
94
135
|
# => 'foo bar baz hello there'
|
|
95
136
|
|
|
137
|
+
# Symbols
|
|
138
|
+
Clsx[:foo, :'bar-baz']
|
|
139
|
+
# => 'foo bar-baz'
|
|
140
|
+
|
|
141
|
+
# Numbers
|
|
142
|
+
Clsx[1, 2, 3]
|
|
143
|
+
# => '1 2 3'
|
|
144
|
+
|
|
96
145
|
# Kitchen sink (with nesting)
|
|
97
146
|
Clsx['foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya']
|
|
98
147
|
# => 'foo bar hello world cya'
|
|
99
148
|
```
|
|
100
149
|
|
|
101
|
-
|
|
150
|
+
## Framework Examples
|
|
151
|
+
|
|
152
|
+
### Rails
|
|
102
153
|
|
|
103
154
|
```erb
|
|
104
|
-
<%# Rails %>
|
|
105
155
|
<%= tag.div class: Clsx['foo', 'baz', 'is-active': @active] do %>
|
|
106
156
|
Hello, world!
|
|
107
157
|
<% end %>
|
|
108
158
|
```
|
|
109
159
|
|
|
160
|
+
### Sinatra
|
|
161
|
+
|
|
110
162
|
```ruby
|
|
111
|
-
# Sinatra
|
|
112
163
|
erb :"<div class='#{Clsx['nav', active: @active]}'>...</div>"
|
|
113
164
|
```
|
|
114
165
|
|
|
166
|
+
### ViewComponent
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class AlertComponent < ViewComponent::Base
|
|
170
|
+
include Clsx::Helper
|
|
171
|
+
|
|
172
|
+
def initialize(variant: :info, dismissible: false)
|
|
173
|
+
@variant = variant
|
|
174
|
+
@dismissible = dismissible
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def classes
|
|
178
|
+
clsx("alert", "alert-#{@variant}", dismissible: @dismissible)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```erb
|
|
184
|
+
<div class="<%= classes %>">...</div>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Tailwind CSS
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class NavLink < ViewComponent::Base
|
|
191
|
+
include Clsx::Helper
|
|
192
|
+
|
|
193
|
+
def initialize(active: false)
|
|
194
|
+
@active = active
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def classes
|
|
198
|
+
clsx(
|
|
199
|
+
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
|
200
|
+
'text-white bg-indigo-600': @active,
|
|
201
|
+
'text-gray-300 hover:text-white hover:bg-gray-700': !@active
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Phlex
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
class Badge < Phlex::HTML
|
|
211
|
+
include Clsx::Helper
|
|
212
|
+
|
|
213
|
+
def initialize(color: :blue, pill: false)
|
|
214
|
+
@color = color
|
|
215
|
+
@pill = pill
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def view_template
|
|
219
|
+
span(class: clsx("badge", "badge-#{@color}", pill: @pill)) { yield }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
115
224
|
## Differences from JavaScript clsx
|
|
116
225
|
|
|
117
|
-
1. **
|
|
226
|
+
1. **Returns `nil`** when no classes apply (not an empty string). This prevents rendering empty `class=""` attributes in template engines that skip `nil`:
|
|
118
227
|
```ruby
|
|
119
|
-
Clsx[
|
|
228
|
+
Clsx[nil, false] # => nil
|
|
120
229
|
```
|
|
121
230
|
|
|
122
|
-
2. **
|
|
231
|
+
2. **Deduplication** — Duplicate classes are automatically removed:
|
|
123
232
|
```ruby
|
|
124
|
-
Clsx[
|
|
233
|
+
Clsx['foo', 'foo'] # => 'foo'
|
|
125
234
|
```
|
|
126
235
|
|
|
127
|
-
3. **
|
|
236
|
+
3. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
|
|
128
237
|
```ruby
|
|
129
|
-
Clsx[''
|
|
238
|
+
Clsx['foo' => 0, bar: []] # => 'foo bar'
|
|
130
239
|
```
|
|
131
240
|
|
|
132
|
-
4. **
|
|
241
|
+
4. **Complex hash keys** — Any valid `clsx` input works as a hash key:
|
|
133
242
|
```ruby
|
|
134
|
-
Clsx[
|
|
243
|
+
Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
|
|
135
244
|
```
|
|
136
245
|
|
|
137
|
-
5. **
|
|
246
|
+
5. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
|
|
138
247
|
```ruby
|
|
139
|
-
Clsx['
|
|
248
|
+
Clsx['', proc {}, -> {}, nil, false, true] # => nil
|
|
140
249
|
```
|
|
141
250
|
|
|
142
251
|
## Development
|
|
143
252
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages.
|
|
151
|
-
|
|
152
|
-
Types: `feat`, `fix`, `perf`, `chore`, `docs`, `refactor`
|
|
253
|
+
```bash
|
|
254
|
+
bin/setup # install dependencies
|
|
255
|
+
bundle exec rake test # run tests
|
|
256
|
+
bundle exec ruby benchmark/run.rb # run benchmarks
|
|
257
|
+
```
|
|
153
258
|
|
|
154
259
|
## Contributing
|
|
155
260
|
|
data/lib/clsx/helper.rb
CHANGED
|
@@ -1,71 +1,82 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# :nodoc:
|
|
4
3
|
module Clsx
|
|
5
|
-
#
|
|
4
|
+
# Mixin providing {#clsx} and {#cn} instance methods.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# include Clsx::Helper
|
|
8
|
+
# clsx('btn', active: @active) # => "btn active"
|
|
6
9
|
module Helper
|
|
7
|
-
#
|
|
8
|
-
# each of which can be Hash, Array, Boolean, String, or Symbol.
|
|
10
|
+
# Build a CSS class string from an arbitrary mix of arguments.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
+
# Falsy values (+nil+, +false+) and standalone +true+ are discarded.
|
|
13
|
+
# Duplicates are eliminated. Returns +nil+ (not +""+) when no classes apply.
|
|
12
14
|
#
|
|
13
|
-
# @param [
|
|
15
|
+
# @param args [String, Symbol, Hash, Array, Numeric, nil, false] class descriptors
|
|
16
|
+
# to merge into a single space-separated string
|
|
17
|
+
# @return [String] space-joined class string
|
|
18
|
+
# @return [nil] when no classes apply
|
|
14
19
|
#
|
|
15
|
-
# @
|
|
20
|
+
# @example Strings and hashes
|
|
21
|
+
# clsx('foo', 'bar') # => "foo bar"
|
|
22
|
+
# clsx(foo: true, bar: false, baz: true) # => "foo baz"
|
|
16
23
|
#
|
|
17
|
-
# @example
|
|
18
|
-
# clsx('
|
|
19
|
-
# clsx(
|
|
20
|
-
# clsx('foo', { bar: false }) # => 'foo'
|
|
21
|
-
# clsx({ bar: true }, 'baz', { bat: false }) # => 'bar baz'
|
|
22
|
-
#
|
|
23
|
-
# @example within a view
|
|
24
|
-
# <div class="<%= clsx('foo', 'bar') %>">
|
|
25
|
-
# <div class="<%= clsx('foo', active: @is_active, 'another-class' => @condition) %>">
|
|
26
|
-
# <%= tag.div class: clsx(%w[foo bar], hidden: @condition) do ... end %>
|
|
27
|
-
#
|
|
28
|
-
# @note Implementation prioritizes performance over readability.
|
|
29
|
-
# Direct class comparisons and explicit conditionals are used
|
|
30
|
-
# instead of more idiomatic Ruby patterns for speed.
|
|
31
|
-
|
|
32
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
24
|
+
# @example Nested arrays
|
|
25
|
+
# clsx('a', ['b', nil, ['c']]) # => "a b c"
|
|
26
|
+
# clsx(%w[foo bar], hidden: true) # => "foo bar hidden"
|
|
33
27
|
def clsx(*args)
|
|
34
28
|
return nil if args.empty?
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if args.size == 1
|
|
38
|
-
arg = args[0]
|
|
39
|
-
klass = arg.class
|
|
40
|
-
|
|
41
|
-
if klass == String
|
|
42
|
-
return arg.empty? ? nil : arg
|
|
43
|
-
elsif klass == Symbol
|
|
44
|
-
return arg.name
|
|
45
|
-
elsif klass == Array && arg.all?(String)
|
|
46
|
-
seen = {}
|
|
47
|
-
arg.each { |s| seen[s] = true unless s.empty? || seen.key?(s) }
|
|
48
|
-
return seen.empty? ? nil : seen.keys.join(' ')
|
|
49
|
-
elsif klass == Hash
|
|
50
|
-
return clsx_simple_hash(arg)
|
|
51
|
-
end
|
|
52
|
-
elsif args.size == 2
|
|
53
|
-
return clsx_string_hash(args[0], args[1]) if args[0].class == String && args[1].class == Hash # rubocop:disable Style/ClassEqualityComparison
|
|
54
|
-
end
|
|
29
|
+
return clsx_single(args[0]) if args.size == 1
|
|
30
|
+
return clsx_string_hash(args[0], args[1]) if args.size == 2 && args[0].is_a?(String) && args[1].is_a?(Hash)
|
|
55
31
|
|
|
56
32
|
seen = {}
|
|
57
33
|
clsx_process(args, seen)
|
|
58
34
|
seen.empty? ? nil : seen.keys.join(' ')
|
|
59
35
|
end
|
|
60
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
61
36
|
|
|
37
|
+
# (see #clsx)
|
|
62
38
|
alias cn clsx
|
|
63
39
|
|
|
64
40
|
private
|
|
65
41
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
42
|
+
# Single-argument fast path — dispatches by type to avoid allocating
|
|
43
|
+
# a +seen+ hash when possible.
|
|
44
|
+
#
|
|
45
|
+
# @param arg [String, Symbol, Hash, Array] single class descriptor
|
|
46
|
+
# @return [String] resolved class string
|
|
47
|
+
# @return [nil] when the argument produces no classes
|
|
48
|
+
def clsx_single(arg)
|
|
49
|
+
return (arg.empty? ? nil : arg) if arg.is_a?(String)
|
|
50
|
+
return arg.name if arg.is_a?(Symbol)
|
|
51
|
+
return clsx_simple_hash(arg) if arg.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
if arg.is_a?(Array)
|
|
54
|
+
return nil if arg.empty?
|
|
55
|
+
|
|
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
|
+
seen = {}
|
|
63
|
+
clsx_process(arg, seen)
|
|
64
|
+
return seen.empty? ? nil : seen.keys.join(' ')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return arg.to_s if arg.is_a?(Numeric)
|
|
68
|
+
return nil if !arg || arg == true || arg.is_a?(Proc)
|
|
69
|
+
|
|
70
|
+
str = arg.to_s
|
|
71
|
+
str.empty? ? nil : str
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Hash-only fast path — no dedup needed since hash keys are unique.
|
|
75
|
+
# Falls back to {#clsx_process} on non-String/Symbol keys.
|
|
76
|
+
#
|
|
77
|
+
# @param hash [Hash{String, Symbol => Boolean}] class-name => condition pairs
|
|
78
|
+
# @return [String] resolved class string
|
|
79
|
+
# @return [nil] when no hash values are truthy
|
|
69
80
|
def clsx_simple_hash(hash)
|
|
70
81
|
return nil if hash.empty?
|
|
71
82
|
|
|
@@ -73,11 +84,10 @@ module Clsx
|
|
|
73
84
|
hash.each do |key, value|
|
|
74
85
|
next unless value
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
if klass == Symbol
|
|
87
|
+
if key.is_a?(Symbol)
|
|
78
88
|
str = key.name
|
|
79
89
|
buf ? (buf << ' ' << str) : (buf = str.dup)
|
|
80
|
-
elsif
|
|
90
|
+
elsif key.is_a?(String)
|
|
81
91
|
next if key.empty?
|
|
82
92
|
|
|
83
93
|
buf ? (buf << ' ' << key) : (buf = key.dup)
|
|
@@ -89,11 +99,14 @@ module Clsx
|
|
|
89
99
|
end
|
|
90
100
|
buf
|
|
91
101
|
end
|
|
92
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
93
102
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
103
|
+
# Fast path for +clsx('base', active: cond)+ — deduplicates only against
|
|
104
|
+
# the base string. Falls back to {#clsx_process} on non-String/Symbol keys.
|
|
105
|
+
#
|
|
106
|
+
# @param str [String] base class name
|
|
107
|
+
# @param hash [Hash{String, Symbol => Boolean}] class-name => condition pairs
|
|
108
|
+
# @return [String] resolved class string
|
|
109
|
+
# @return [nil] when no classes apply
|
|
97
110
|
def clsx_string_hash(str, hash)
|
|
98
111
|
return clsx_simple_hash(hash) if str.empty?
|
|
99
112
|
|
|
@@ -101,11 +114,10 @@ module Clsx
|
|
|
101
114
|
hash.each do |key, value|
|
|
102
115
|
next unless value
|
|
103
116
|
|
|
104
|
-
|
|
105
|
-
if klass == Symbol
|
|
117
|
+
if key.is_a?(Symbol)
|
|
106
118
|
s = key.name
|
|
107
119
|
buf << ' ' << s unless s == str
|
|
108
|
-
elsif
|
|
120
|
+
elsif key.is_a?(String)
|
|
109
121
|
buf << ' ' << key unless key.empty? || key == str
|
|
110
122
|
else
|
|
111
123
|
seen = { str => true }
|
|
@@ -115,37 +127,43 @@ module Clsx
|
|
|
115
127
|
end
|
|
116
128
|
buf
|
|
117
129
|
end
|
|
118
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
119
130
|
|
|
120
|
-
#
|
|
131
|
+
# General-purpose recursive walker. Processes hash keys inline for simple
|
|
132
|
+
# types (String/Symbol) to avoid deferred-array allocation. Only complex
|
|
133
|
+
# keys (Array, nested Hash) recurse.
|
|
134
|
+
#
|
|
135
|
+
# @param args [Array<String, Symbol, Hash, Array, Numeric, nil, false>] nested arguments
|
|
136
|
+
# @param seen [Hash{String => true}] accumulator for deduplication
|
|
137
|
+
# @return [void]
|
|
121
138
|
def clsx_process(args, seen)
|
|
122
|
-
deferred = nil
|
|
123
|
-
|
|
124
139
|
args.each do |arg|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
seen[arg] = true
|
|
129
|
-
elsif
|
|
130
|
-
str = arg.name
|
|
131
|
-
seen[str] = true unless seen.key?(str)
|
|
132
|
-
elsif klass == Array
|
|
140
|
+
if arg.is_a?(String)
|
|
141
|
+
seen[arg] = true unless arg.empty?
|
|
142
|
+
elsif arg.is_a?(Symbol)
|
|
143
|
+
seen[arg.name] = true
|
|
144
|
+
elsif arg.is_a?(Array)
|
|
133
145
|
clsx_process(arg, seen)
|
|
134
|
-
elsif
|
|
135
|
-
arg.each
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
elsif arg.is_a?(Hash)
|
|
147
|
+
arg.each do |key, value|
|
|
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
|
|
160
|
+
elsif !arg || arg == true || arg.is_a?(Proc)
|
|
140
161
|
next
|
|
141
162
|
else
|
|
142
163
|
str = arg.to_s
|
|
143
|
-
seen[str] = true unless str.empty?
|
|
164
|
+
seen[str] = true unless str.empty?
|
|
144
165
|
end
|
|
145
166
|
end
|
|
146
|
-
|
|
147
|
-
clsx_process(deferred, seen) if deferred
|
|
148
167
|
end
|
|
149
|
-
# rubocop:enable Style/MultipleComparison, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
150
168
|
end
|
|
151
169
|
end
|
data/lib/clsx/version.rb
CHANGED
data/lib/clsx.rb
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
require_relative 'clsx/version'
|
|
4
4
|
require_relative 'clsx/helper'
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Construct CSS class strings conditionally.
|
|
7
|
+
# Ruby port of the JavaScript {https://github.com/lukeed/clsx clsx} package.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Clsx['foo', 'bar'] # => "foo bar"
|
|
11
|
+
# Clsx['btn', active: true] # => "btn active"
|
|
12
|
+
# Cn['hidden', visible: false] # => "hidden"
|
|
7
13
|
module Clsx
|
|
8
14
|
extend Helper
|
|
9
15
|
|
|
16
|
+
# (see Helper#clsx)
|
|
10
17
|
def self.[](*)
|
|
11
18
|
clsx(*)
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
# Short alias — only defined if
|
|
16
|
-
unless Object.const_defined?(:Cn)
|
|
17
|
-
# :nodoc:
|
|
18
|
-
module Cn
|
|
19
|
-
def self.[](*)
|
|
20
|
-
Clsx[*]
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
22
|
+
# Short alias — only defined if +Cn+ is not already taken.
|
|
23
|
+
Cn = Clsx unless Object.const_defined?(:Cn)
|
metadata
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clsx-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
12
|
+
description: Build CSS class strings from conditional expressions, hashes, arrays,
|
|
13
|
+
or nested structures. Framework-agnostic. Perfect for ViewComponent, Phlex, and
|
|
14
|
+
Tailwind CSS. Works with any Ruby codebase.
|
|
13
15
|
email:
|
|
14
16
|
- leonid@svyatov.com
|
|
15
17
|
executables: []
|
|
@@ -17,7 +19,6 @@ extensions: []
|
|
|
17
19
|
extra_rdoc_files: []
|
|
18
20
|
files:
|
|
19
21
|
- CHANGELOG.md
|
|
20
|
-
- CLAUDE.md
|
|
21
22
|
- LICENSE.txt
|
|
22
23
|
- README.md
|
|
23
24
|
- lib/clsx.rb
|
|
@@ -46,5 +47,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
46
47
|
requirements: []
|
|
47
48
|
rubygems_version: 4.0.6
|
|
48
49
|
specification_version: 4
|
|
49
|
-
summary:
|
|
50
|
+
summary: The fastest conditional CSS class builder for Ruby
|
|
50
51
|
test_files: []
|
data/CLAUDE.md
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
clsx-ruby is a Ruby gem that provides a utility (`clsx`/`cn`) for constructing CSS class strings conditionally. It's a Ruby port of the JavaScript [clsx](https://github.com/lukeed/clsx) package, adapted for Ruby conventions. Framework-agnostic — works with Rails, Sinatra, Hanami, or plain Ruby.
|
|
8
|
-
|
|
9
|
-
## Common Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# Run all tests and linting (default rake task)
|
|
13
|
-
bundle exec rake
|
|
14
|
-
|
|
15
|
-
# Run tests only
|
|
16
|
-
bundle exec rake test
|
|
17
|
-
|
|
18
|
-
# Run a single test file
|
|
19
|
-
bundle exec ruby -Itest test/clsx/helper_test.rb
|
|
20
|
-
|
|
21
|
-
# Run a specific test method
|
|
22
|
-
bundle exec ruby -Itest test/clsx/helper_test.rb -n test_with_strings
|
|
23
|
-
|
|
24
|
-
# Run linter
|
|
25
|
-
bundle exec rake rubocop
|
|
26
|
-
|
|
27
|
-
# Run benchmark
|
|
28
|
-
bundle exec ruby benchmark/run.rb
|
|
29
|
-
|
|
30
|
-
# Install dependencies
|
|
31
|
-
bin/setup
|
|
32
|
-
|
|
33
|
-
# Release a new version (update version.rb first)
|
|
34
|
-
# Builds gem, creates git tag, pushes to rubygems.org
|
|
35
|
-
# OTP is fetched automatically from 1Password
|
|
36
|
-
bundle exec rake release
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Architecture
|
|
40
|
-
|
|
41
|
-
The gem has a minimal structure:
|
|
42
|
-
|
|
43
|
-
- `lib/clsx.rb` - Entry point; extends `Clsx` with `Helper`, defines `Clsx[]` and `Cn[]` shortcuts
|
|
44
|
-
- `lib/clsx/helper.rb` - Core implementation with `clsx` method and `cn` alias
|
|
45
|
-
- `lib/clsx/version.rb` - Version constant
|
|
46
|
-
|
|
47
|
-
### API
|
|
48
|
-
|
|
49
|
-
- **`Clsx['foo', bar: true]`** — primary bracket API via `self.[]`
|
|
50
|
-
- **`Cn['foo', bar: true]`** — short alias (defined only if `Cn` constant is not taken)
|
|
51
|
-
- **`Clsx.clsx(...)`** / **`Clsx.cn(...)`** — module methods
|
|
52
|
-
- **`include Clsx::Helper`** — mixin giving `clsx()` and `cn()` instance methods
|
|
53
|
-
|
|
54
|
-
The helper uses an optimized algorithm with fast-paths for common cases (single string, string array, simple hash) and Hash-based deduplication for complex inputs.
|
|
55
|
-
|
|
56
|
-
## Key Behaviors
|
|
57
|
-
|
|
58
|
-
- Returns `nil` (not empty string) when no classes apply — prevents rendering empty `class=""` attributes
|
|
59
|
-
- Eliminates duplicate classes automatically
|
|
60
|
-
- Ruby falsy values are only `false` and `nil` (unlike JS, `0`, `''`, `[]`, `{}` are truthy)
|
|
61
|
-
- Ignores `Proc`/lambda objects and boolean `true` values
|
|
62
|
-
- Supports complex hash keys like `{ %w[foo bar] => true }` which resolve recursively
|
|
63
|
-
|
|
64
|
-
## Commit Convention
|
|
65
|
-
|
|
66
|
-
Uses [Conventional Commits](https://www.conventionalcommits.org/): `feat`, `fix`, `perf`, `chore`, `docs`, `refactor`
|