clsx-ruby 1.1.0 → 1.1.1

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: 043652e6d2bc00c77d0409b45cb15f361cd1fb417af14db62892be42ed2a1815
4
+ data.tar.gz: dbacda2ec82ed045800345914fec73339d1dfee64df87e33305a5a8b5cf7c118
5
5
  SHA512:
6
- metadata.gz: 5cbb6c36d0920943123f9e19c978a8ef4914669ca6b71666f770883c865ca4c2dd6d3f089ced1612d62532b881c3492df656d4381bb85533dd635c7af4009c0c
7
- data.tar.gz: d6ac365bce6825166fdf51303a87398f20997075535babf65a28686dea562f46586bc0c6e9418bfac77b9307e8820b377f4b5ce2265b21d7360c405928cd524f
6
+ metadata.gz: 26c9443192380c6b8415923df85df04d78add5cead6a676d78739410261cbeb277f8e6c4856b15761355dd61becfc09ad6aec7870d530f78bb5d3fcfbd358893
7
+ data.tar.gz: 919462b5126de045d8a2b1d63ef5e3da4fb04156ef62c9c3e4985922d05dd041997fcbc473f1aab7495d61914e87b09cdf88b2f1448f0fb80b8c3c3006115763
data/CHANGELOG.md CHANGED
@@ -1,14 +1,49 @@
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.1
11
+
12
+ ### Added
13
+
14
+ - YARD documentation to all public and private methods
15
+ - Rails 8.1.2 `class_names` as a third benchmark competitor
16
+ - ViewComponent and Phlex examples in README
17
+ - Memory and profiling benchmark scripts (`benchmark/memory.rb`, `benchmark/profile.rb`, `benchmark/stackprof_run.rb`)
18
+
19
+ ### Changed
20
+
21
+ - Replaced `class ==` type checks with idiomatic `is_a?` early returns
22
+ - Extracted `clsx_single` method for clearer single-argument dispatch
23
+ - Simplified `Cn` alias from wrapper module to `Cn = Clsx`
24
+ - Removed redundant `seen.key?` guards in `clsx_process`
25
+ - Reduced allocations in `clsx_single`: pass arrays directly to `clsx_process` instead of wrapping, handle edge types inline
26
+ - Moved inline rubocop disables to `.rubocop.yml` config
27
+ - Updated benchmark baseline to compare against previous version, not ancient one
28
+ - Rewrote README with benchmark numbers and feature comparison table
29
+ - 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.
30
+
3
31
  ## v1.1.0
4
32
 
5
- - Optimized hash-only path: skip dedup Hash since hash keys are unique by definition (+8%)
33
+ ### Added
34
+
6
35
  - New fast path for `clsx('base', active: cond)` string + hash pattern (+69%)
7
- - Added `string + hash` benchmark scenario
36
+ - `string + hash` benchmark scenario
37
+
38
+ ### Changed
39
+
40
+ - Optimized hash-only path: skip dedup Hash since hash keys are unique by definition (+8%)
8
41
  - Use `str.dup` instead of `String.new(str)` for buffer init (+12-18% on hash paths)
9
42
 
10
43
  ## v1.0.0
11
44
 
45
+ ### Added
46
+
12
47
  - Initial release as standalone framework-agnostic gem
13
48
  - Extracted from clsx-rails v2.0.0
14
49
  - API: `Clsx['foo', bar: true]`, `Cn['foo', bar: true]`, `include Clsx::Helper`
data/CLAUDE.md CHANGED
@@ -61,6 +61,78 @@ The helper uses an optimized algorithm with fast-paths for common cases (single
61
61
  - Ignores `Proc`/lambda objects and boolean `true` values
62
62
  - Supports complex hash keys like `{ %w[foo bar] => true }` which resolve recursively
63
63
 
64
+ ## Benchmarking
65
+
66
+ `benchmark/original.rb` contains the **previous version** of the algorithm for comparison. It must always reflect the last committed version from the main branch — not some ancient baseline.
67
+
68
+ **Rule:** Before making any algorithm or performance change to `lib/clsx/helper.rb`, copy the current main-branch implementation into `benchmark/original.rb` (wrapping in `module ClsxOriginal` — method names stay the same, no renaming needed). This ensures `bundle exec ruby benchmark/run.rb` compares the new code against its immediate predecessor, giving meaningful before/after numbers.
69
+
64
70
  ## Commit Convention
65
71
 
66
- Uses [Conventional Commits](https://www.conventionalcommits.org/): `feat`, `fix`, `perf`, `chore`, `docs`, `refactor`
72
+ This project follows [Conventional Commits v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/).
73
+
74
+ Format: `<type>[optional scope]: <description>`
75
+
76
+ ### Types
77
+
78
+ | Type | Description | Version bump |
79
+ |------|-------------|--------------|
80
+ | `feat` | New feature | MINOR |
81
+ | `fix` | Bug fix | PATCH |
82
+ | `docs` | Documentation only | — |
83
+ | `style` | Formatting, whitespace | — |
84
+ | `refactor` | Code change (no feature/fix) | — |
85
+ | `perf` | Performance improvement | — |
86
+ | `test` | Adding/fixing tests | — |
87
+ | `build` | Build system or dependencies | — |
88
+ | `ci` | CI configuration | — |
89
+ | `chore` | Maintenance tasks | — |
90
+
91
+ ### Breaking Changes
92
+
93
+ Use `!` after type or add `BREAKING CHANGE:` footer. Breaking changes trigger a MAJOR version bump.
94
+
95
+ ## Changelog Format
96
+
97
+ This project follows [Keep a Changelog v1.1.0](https://keepachangelog.com/en/1.1.0/).
98
+
99
+ Allowed categories in **required order**:
100
+
101
+ 1. **Added** — new features
102
+ 2. **Changed** — changes to existing functionality
103
+ 3. **Deprecated** — soon-to-be removed features
104
+ 4. **Removed** — removed features
105
+ 5. **Fixed** — bug fixes
106
+ 6. **Security** — vulnerability fixes
107
+
108
+ Rules:
109
+ - Categories must appear in the order listed above within each release section
110
+ - Each category must appear **at most once** per release section — always append to an existing category rather than creating a duplicate
111
+ - Do NOT use non-standard categories like "Updated", "Internal", or "Breaking changes"
112
+ - Breaking changes should be prefixed with **BREAKING:** within the relevant category (typically Changed or Removed)
113
+
114
+ `CHANGELOG.md` must stay current on every feature branch. After each commit, ensure the `## Unreleased` section at the top accurately reflects all user-facing changes on the branch. Add the section if it doesn't exist. Keep entries concise — one bullet per logical change. On release, the `## Unreleased` heading gets replaced with the version number.
115
+
116
+ The unreleased section describes the **net result** compared to the last release, not a history of intermediate steps. When a later change supersedes an earlier one, update or remove the stale bullet — don't accumulate entries that no longer reflect reality.
117
+
118
+ ## Documentation Style
119
+
120
+ All classes and methods must have YARD documentation. Follow these conventions:
121
+
122
+ - Always leave a **blank line** between the main description and `@` attributes (params, return, etc.)
123
+ - Document all public methods with description, params, and return types
124
+ - Document all private methods with params and return types, add description for complex logic
125
+ - Include `@example` blocks for non-obvious usage patterns
126
+ - **Omit descriptions that just repeat the code** — if the method name and signature make it obvious, only include `@param`, `@return` tags without a description
127
+
128
+ ## Releasing a New Version
129
+
130
+ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html):
131
+ - **MAJOR** — breaking changes (incompatible API changes)
132
+ - **MINOR** — new features (backwards-compatible)
133
+ - **PATCH** — bug fixes (backwards-compatible)
134
+
135
+ 1. Update `lib/clsx/version.rb` with the new version number
136
+ 2. Update `CHANGELOG.md`: change `## Unreleased` to `## vX.Y.Z` and add new empty `## Unreleased` section
137
+ 3. Commit changes: `chore: bump version to X.Y.Z`
138
+ 4. Release: `bundle exec rake release`
data/README.md CHANGED
@@ -1,16 +1,10 @@
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 Ruby utility for constructing CSS class strings conditionally. Perfect for Tailwind CSS utility classes. Zero dependencies.
4
4
 
5
- Ruby port of the JavaScript [clsx](https://github.com/lukeed/clsx) package. Works with Rails, Sinatra, Hanami, or plain Ruby.
5
+ Ruby port of the JavaScript [clsx](https://github.com/lukeed/clsx) package — a faster, smarter alternative to Rails `class_names`. Works with Rails, Sinatra, Hanami, or plain Ruby.
6
6
 
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
7
+ ## Quick Start
14
8
 
15
9
  ```bash
16
10
  bundle add clsx-ruby
@@ -19,9 +13,51 @@ bundle add clsx-ruby
19
13
  Or add it manually to the Gemfile:
20
14
 
21
15
  ```ruby
22
- gem 'clsx-ruby', '~> 1.0'
16
+ gem 'clsx-ruby', '~> 1.1'
17
+ ```
18
+
19
+ Then use it:
20
+
21
+ ```ruby
22
+ require 'clsx'
23
+
24
+ Clsx['btn', 'btn-primary', active: is_active, disabled: is_disabled]
25
+ # => "btn btn-primary active" (when is_active is truthy, is_disabled is falsy)
23
26
  ```
24
27
 
28
+ ## Why clsx-ruby?
29
+
30
+ ### Blazing fast
31
+
32
+ **3–8x faster** than Rails `class_names` across every scenario:
33
+
34
+ | Scenario | clsx-ruby | Rails `class_names` | Speedup |
35
+ |---|---|---|---|
36
+ | Single string | 7.6M i/s | 911K i/s | **8.5x** |
37
+ | String + hash | 2.4M i/s | 580K i/s | **4.1x** |
38
+ | String array | 1.4M i/s | 357K i/s | **4.0x** |
39
+ | Multiple strings | 1.5M i/s | 414K i/s | **3.7x** |
40
+ | Hash | 2.2M i/s | 670K i/s | **3.3x** |
41
+ | Mixed types | 852K i/s | 367K i/s | **2.3x** |
42
+
43
+ <sup>Ruby 4.0.1, Apple M1 Pro. Reproduce: `bundle exec ruby benchmark/run.rb`</sup>
44
+
45
+ ### More feature-rich than `class_names`
46
+
47
+ | Feature | clsx-ruby | Rails `class_names` |
48
+ |---|---|---|
49
+ | Conditional classes | ✅ | ✅ |
50
+ | Auto-deduplication | ✅ | ✅ |
51
+ | 3–8× faster | ✅ | ❌ |
52
+ | Returns `nil` when empty | ✅ | ❌ (returns `""`) |
53
+ | Complex hash keys | ✅ | ❌ |
54
+ | Framework-agnostic | ✅ | ❌ |
55
+ | Zero dependencies | ✅ | ❌ |
56
+
57
+ ### Tiny footprint
58
+
59
+ ~100 lines of code. Zero runtime dependencies. Ruby 3.2+.
60
+
25
61
  ## Usage
26
62
 
27
63
  ### Bracket API (recommended)
@@ -93,63 +129,131 @@ Clsx[['foo', nil, false, 'bar']]
93
129
  Clsx[['foo'], ['', nil, false, 'bar'], [['baz', [['hello'], 'there']]]]
94
130
  # => 'foo bar baz hello there'
95
131
 
132
+ # Symbols
133
+ Clsx[:foo, :'bar-baz']
134
+ # => 'foo bar-baz'
135
+
136
+ # Numbers
137
+ Clsx[1, 2, 3]
138
+ # => '1 2 3'
139
+
96
140
  # Kitchen sink (with nesting)
97
141
  Clsx['foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya']
98
142
  # => 'foo bar hello world cya'
99
143
  ```
100
144
 
101
- ### Framework examples
145
+ ## Framework Examples
146
+
147
+ ### Rails
102
148
 
103
149
  ```erb
104
- <%# Rails %>
105
150
  <%= tag.div class: Clsx['foo', 'baz', 'is-active': @active] do %>
106
151
  Hello, world!
107
152
  <% end %>
108
153
  ```
109
154
 
155
+ ### Sinatra
156
+
110
157
  ```ruby
111
- # Sinatra
112
158
  erb :"<div class='#{Clsx['nav', active: @active]}'>...</div>"
113
159
  ```
114
160
 
161
+ ### ViewComponent
162
+
163
+ ```ruby
164
+ class AlertComponent < ViewComponent::Base
165
+ include Clsx::Helper
166
+
167
+ def initialize(variant: :info, dismissible: false)
168
+ @variant = variant
169
+ @dismissible = dismissible
170
+ end
171
+
172
+ def classes
173
+ clsx("alert", "alert-#{@variant}", dismissible: @dismissible)
174
+ end
175
+ end
176
+ ```
177
+
178
+ ```erb
179
+ <div class="<%= classes %>">...</div>
180
+ ```
181
+
182
+ ### Tailwind CSS
183
+
184
+ ```ruby
185
+ class NavLink < ViewComponent::Base
186
+ include Clsx::Helper
187
+
188
+ def initialize(active: false)
189
+ @active = active
190
+ end
191
+
192
+ def classes
193
+ clsx(
194
+ 'px-3 py-2 rounded-md text-sm font-medium transition-colors',
195
+ 'text-white bg-indigo-600': @active,
196
+ 'text-gray-300 hover:text-white hover:bg-gray-700': !@active
197
+ )
198
+ end
199
+ end
200
+ ```
201
+
202
+ ### Phlex
203
+
204
+ ```ruby
205
+ class Badge < Phlex::HTML
206
+ include Clsx::Helper
207
+
208
+ def initialize(color: :blue, pill: false)
209
+ @color = color
210
+ @pill = pill
211
+ end
212
+
213
+ def view_template
214
+ span(class: clsx("badge", "badge-#{@color}", pill: @pill)) { yield }
215
+ end
216
+ end
217
+ ```
218
+
115
219
  ## Differences from JavaScript clsx
116
220
 
117
- 1. **Falsy values** In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
221
+ 1. **Returns `nil`** when no classes apply (not an empty string). This prevents rendering empty `class=""` attributes in template engines that skip `nil`:
118
222
  ```ruby
119
- Clsx['foo' => 0, bar: []] # => 'foo bar'
223
+ Clsx[nil, false] # => nil
120
224
  ```
121
225
 
122
- 2. **Complex hash keys** — Any valid `clsx` input works as a hash key:
226
+ 2. **Deduplication** — Duplicate classes are automatically removed:
123
227
  ```ruby
124
- Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
228
+ Clsx['foo', 'foo'] # => 'foo'
125
229
  ```
126
230
 
127
- 3. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
231
+ 3. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
128
232
  ```ruby
129
- Clsx['', proc {}, -> {}, nil, false, true] # => nil
233
+ Clsx['foo' => 0, bar: []] # => 'foo bar'
130
234
  ```
131
235
 
132
- 4. **Returns `nil`** when no classes apply (not an empty string). This prevents rendering empty `class=""` attributes in template engines that skip `nil`:
236
+ 4. **Complex hash keys** Any valid `clsx` input works as a hash key:
133
237
  ```ruby
134
- Clsx[nil, false] # => nil
238
+ Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
135
239
  ```
136
240
 
137
- 5. **Deduplication** — Duplicate classes are automatically removed:
241
+ 5. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
138
242
  ```ruby
139
- Clsx['foo', 'foo'] # => 'foo'
243
+ Clsx['', proc {}, -> {}, nil, false, true] # => nil
140
244
  ```
141
245
 
142
- ## Development
143
-
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`.
246
+ ## Rails Integration
147
247
 
148
- ## Conventional Commits
248
+ For automatic Rails view helper integration (adds `clsx` and `cn` helpers to all views), see [clsx-rails](https://github.com/svyatov/clsx-rails).
149
249
 
150
- This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages.
250
+ ## Development
151
251
 
152
- Types: `feat`, `fix`, `perf`, `chore`, `docs`, `refactor`
252
+ ```bash
253
+ bin/setup # install dependencies
254
+ bundle exec rake test # run tests
255
+ bundle exec ruby benchmark/run.rb # run benchmarks
256
+ ```
153
257
 
154
258
  ## Contributing
155
259
 
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.1'
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,7 +1,7 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov