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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49ae33f9c65cab86b628e1ed9cb764242f13397135caafa48a9a431f5f3d0a78
4
- data.tar.gz: a59da0c73f12ed098f411777c14986f807dccb635ea6055ecece2a08c6845f00
3
+ metadata.gz: 3f20a56982120e4142d5932688887cc7f4b38a3596e01ce8aacf1f7b5b15f8d4
4
+ data.tar.gz: ebbd199c2d2ad139c2dda52680953778092b21708e01aef7b09d4619d932333a
5
5
  SHA512:
6
- metadata.gz: 5cbb6c36d0920943123f9e19c978a8ef4914669ca6b71666f770883c865ca4c2dd6d3f089ced1612d62532b881c3492df656d4381bb85533dd635c7af4009c0c
7
- data.tar.gz: d6ac365bce6825166fdf51303a87398f20997075535babf65a28686dea562f46586bc0c6e9418bfac77b9307e8820b377f4b5ce2265b21d7360c405928cd524f
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
- - Optimized hash-only path: skip dedup Hash since hash keys are unique by definition (+8%)
40
+ ### Added
41
+
6
42
  - New fast path for `clsx('base', active: cond)` string + hash pattern (+69%)
7
- - Added `string + hash` benchmark scenario
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 [![Gem Version](https://img.shields.io/gem/v/clsx-ruby)](https://rubygems.org/gems/clsx-ruby) [![CI](https://github.com/svyatov/clsx-ruby/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/svyatov/clsx-ruby/actions?query=workflow%3ACI) [![GitHub License](https://img.shields.io/github/license/svyatov/clsx-ruby)](LICENSE.txt)
1
+ # clsx-ruby [![Gem Version](https://img.shields.io/gem/v/clsx-ruby)](https://rubygems.org/gems/clsx-ruby) [![Codecov](https://img.shields.io/codecov/c/github/svyatov/clsx-ruby)](https://app.codecov.io/gh/svyatov/clsx-ruby) [![CI](https://github.com/svyatov/clsx-ruby/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/svyatov/clsx-ruby/actions?query=workflow%3ACI) [![GitHub License](https://img.shields.io/github/license/svyatov/clsx-ruby)](LICENSE.txt)
2
2
 
3
- > A tiny, framework-agnostic utility for constructing CSS class strings conditionally.
3
+ > The fastest, framework-agnostic conditional CSS class builder for Ruby.
4
+ > Perfect for ViewComponent, Phlex, Tailwind CSS or just standalone.
4
5
 
5
- Ruby port of the JavaScript [clsx](https://github.com/lukeed/clsx) package. Works with Rails, Sinatra, Hanami, or plain Ruby.
6
+ Inspired by the JavaScript [clsx](https://github.com/lukeed/clsx) package. Works with any Ruby codebase.
6
7
 
7
- For automatic Rails view helper integration, see [clsx-rails](https://github.com/svyatov/clsx-rails).
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.0'
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
- ### Framework examples
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. **Falsy values** In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
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['foo' => 0, bar: []] # => 'foo bar'
228
+ Clsx[nil, false] # => nil
120
229
  ```
121
230
 
122
- 2. **Complex hash keys** — Any valid `clsx` input works as a hash key:
231
+ 2. **Deduplication** — Duplicate classes are automatically removed:
123
232
  ```ruby
124
- Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
233
+ Clsx['foo', 'foo'] # => 'foo'
125
234
  ```
126
235
 
127
- 3. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
236
+ 3. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
128
237
  ```ruby
129
- Clsx['', proc {}, -> {}, nil, false, true] # => nil
238
+ Clsx['foo' => 0, bar: []] # => 'foo bar'
130
239
  ```
131
240
 
132
- 4. **Returns `nil`** when no classes apply (not an empty string). This prevents rendering empty `class=""` attributes in template engines that skip `nil`:
241
+ 4. **Complex hash keys** Any valid `clsx` input works as a hash key:
133
242
  ```ruby
134
- Clsx[nil, false] # => nil
243
+ Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
135
244
  ```
136
245
 
137
- 5. **Deduplication** — Duplicate classes are automatically removed:
246
+ 5. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
138
247
  ```ruby
139
- Clsx['foo', 'foo'] # => 'foo'
248
+ Clsx['', proc {}, -> {}, nil, false, true] # => nil
140
249
  ```
141
250
 
142
251
  ## Development
143
252
 
144
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt.
145
-
146
- There is a benchmark suite in the `benchmark` directory. Run it with `bundle exec ruby benchmark/run.rb`.
147
-
148
- ## Conventional Commits
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
- # :nodoc:
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
- # The clsx function can take any number of arguments,
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
- # **Important**
11
- # Any falsy values are discarded! Standalone Boolean values are discarded as well.
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 [Mixed] args
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
- # @return [String] the joined class names
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('foo', 'bar') # => 'foo bar'
19
- # clsx(true, { bar: true }) # => 'bar'
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
- # Fast path: single argument (most common cases)
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
- # Hash-only path — no dedup hash needed (hash keys are unique by definition).
67
- # Builds result string directly with <<.
68
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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
- klass = key.class
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 klass == String
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
- # String + Hash fast path: clsx('base', active: cond).
95
- # Builds result string directly, dedup only against base string.
96
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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
- klass = key.class
105
- if klass == Symbol
117
+ if key.is_a?(Symbol)
106
118
  s = key.name
107
119
  buf << ' ' << s unless s == str
108
- elsif klass == String
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
- # rubocop:disable Style/MultipleComparison, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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
- klass = arg.class
126
-
127
- if klass == String
128
- seen[arg] = true unless arg.empty? || seen.key?(arg)
129
- elsif klass == Symbol
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 klass == Hash
135
- arg.each { |key, value| (deferred ||= []) << key if value }
136
- elsif klass == Integer || klass == Float
137
- str = arg.to_s
138
- seen[str] = true unless seen.key?(str)
139
- elsif klass == NilClass || klass == FalseClass || klass == TrueClass || klass == Proc
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? || seen.key?(str)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clsx
4
- VERSION = '1.1.0'
4
+ VERSION = '1.1.2'
5
5
  end
data/lib/clsx.rb CHANGED
@@ -3,21 +3,21 @@
3
3
  require_relative 'clsx/version'
4
4
  require_relative 'clsx/helper'
5
5
 
6
- # :nodoc:
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 `Cn` is not already taken
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.0
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: A tiny utility for constructing CSS class strings conditionally
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: clsx / classnames for Ruby
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`