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 +4 -4
- data/CHANGELOG.md +37 -2
- data/CLAUDE.md +73 -1
- data/README.md +136 -32
- data/lib/clsx/helper.rb +98 -80
- data/lib/clsx/version.rb +1 -1
- data/lib/clsx.rb +10 -10
- 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: 043652e6d2bc00c77d0409b45cb15f361cd1fb417af14db62892be42ed2a1815
|
|
4
|
+
data.tar.gz: dbacda2ec82ed045800345914fec73339d1dfee64df87e33305a5a8b5cf7c118
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
33
|
+
### Added
|
|
34
|
+
|
|
6
35
|
- New fast path for `clsx('base', active: cond)` string + hash pattern (+69%)
|
|
7
|
-
-
|
|
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
|
-
|
|
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 [](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 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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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. **
|
|
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[
|
|
223
|
+
Clsx[nil, false] # => nil
|
|
120
224
|
```
|
|
121
225
|
|
|
122
|
-
2. **
|
|
226
|
+
2. **Deduplication** — Duplicate classes are automatically removed:
|
|
123
227
|
```ruby
|
|
124
|
-
Clsx[
|
|
228
|
+
Clsx['foo', 'foo'] # => 'foo'
|
|
125
229
|
```
|
|
126
230
|
|
|
127
|
-
3. **
|
|
231
|
+
3. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
|
|
128
232
|
```ruby
|
|
129
|
-
Clsx[''
|
|
233
|
+
Clsx['foo' => 0, bar: []] # => 'foo bar'
|
|
130
234
|
```
|
|
131
235
|
|
|
132
|
-
4. **
|
|
236
|
+
4. **Complex hash keys** — Any valid `clsx` input works as a hash key:
|
|
133
237
|
```ruby
|
|
134
|
-
Clsx[
|
|
238
|
+
Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
|
|
135
239
|
```
|
|
136
240
|
|
|
137
|
-
5. **
|
|
241
|
+
5. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
|
|
138
242
|
```ruby
|
|
139
|
-
Clsx['
|
|
243
|
+
Clsx['', proc {}, -> {}, nil, false, true] # => nil
|
|
140
244
|
```
|
|
141
245
|
|
|
142
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
250
|
+
## Development
|
|
151
251
|
|
|
152
|
-
|
|
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
|
-
#
|
|
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)
|