clsx-ruby 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b9a68c03880d32a04dc1c721e0ee3009ac8a276f906da830e4866e2afae6ce92
4
+ data.tar.gz: 24155294bbdca977a8358ef7f6296febfdc86c2ca61d4faaf5b7e49b7f6d0d2a
5
+ SHA512:
6
+ metadata.gz: 34de1db1360cae0f43ac95c1c5e1cb65d53294a313d5ad232afd79fe2b785e9ffab14f5b6bec527058f0cacc13354a000db1ea3479f43f0f6e17e5240a245c90
7
+ data.tar.gz: 4236efda398f4977ebb215ef492fd3965eff434efe9a8bccb9da4fb06023bd0bf8f32c02fabc962129a0fce7cbae34950c7936e0b3334e5da48219ddb15dc7ea
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## v1.0.0
4
+
5
+ - Initial release as standalone framework-agnostic gem
6
+ - Extracted from clsx-rails v2.0.0
7
+ - API: `Clsx['foo', bar: true]`, `Cn['foo', bar: true]`, `include Clsx::Helper`
8
+ - No runtime dependencies
data/CLAUDE.md ADDED
@@ -0,0 +1,66 @@
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`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024-2026 Leonid Svyatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
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)
2
+
3
+ > A tiny, framework-agnostic utility for constructing CSS class strings conditionally.
4
+
5
+ Ruby port of the JavaScript [clsx](https://github.com/lukeed/clsx) package. Works with Rails, Sinatra, Hanami, or plain Ruby.
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
14
+
15
+ ```bash
16
+ bundle add clsx-ruby
17
+ ```
18
+
19
+ Or add it manually to the Gemfile:
20
+
21
+ ```ruby
22
+ gem 'clsx-ruby', '~> 1.0'
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Bracket API (recommended)
28
+
29
+ ```ruby
30
+ require 'clsx'
31
+
32
+ Clsx['foo', 'bar']
33
+ # => 'foo bar'
34
+
35
+ Clsx['foo', bar: true, baz: false]
36
+ # => 'foo bar'
37
+
38
+ Clsx['btn', 'btn-primary', active: is_active, disabled: is_disabled]
39
+ # => 'btn btn-primary active' (when is_active is truthy, is_disabled is falsy)
40
+ ```
41
+
42
+ ### Short alias
43
+
44
+ ```ruby
45
+ Cn['foo', bar: true]
46
+ # => 'foo bar'
47
+ ```
48
+
49
+ `Cn` is defined only if the constant is not already taken.
50
+
51
+ ### Mixin
52
+
53
+ ```ruby
54
+ include Clsx::Helper
55
+
56
+ clsx('foo', 'bar')
57
+ # => 'foo bar'
58
+
59
+ cn(hidden: @hidden, 'text-bold': @bold)
60
+ # => 'hidden text-bold' (when both are truthy)
61
+ ```
62
+
63
+ ### Module methods
64
+
65
+ ```ruby
66
+ Clsx.clsx('foo', 'bar')
67
+ # => 'foo bar'
68
+
69
+ Clsx.cn('foo', bar: true)
70
+ # => 'foo bar'
71
+ ```
72
+
73
+ ### Input types
74
+
75
+ ```ruby
76
+ # Strings (variadic)
77
+ Clsx['foo', true && 'bar', 'baz']
78
+ # => 'foo bar baz'
79
+
80
+ # Hashes
81
+ Clsx[foo: true, bar: false, baz: a_truthy_method]
82
+ # => 'foo baz'
83
+
84
+ # Hashes (variadic)
85
+ Clsx[{ foo: true }, { bar: false }, nil, { '--foobar': 'hello' }]
86
+ # => 'foo --foobar'
87
+
88
+ # Arrays
89
+ Clsx[['foo', nil, false, 'bar']]
90
+ # => 'foo bar'
91
+
92
+ # Arrays (variadic)
93
+ Clsx[['foo'], ['', nil, false, 'bar'], [['baz', [['hello'], 'there']]]]
94
+ # => 'foo bar baz hello there'
95
+
96
+ # Kitchen sink (with nesting)
97
+ Clsx['foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya']
98
+ # => 'foo bar hello world cya'
99
+ ```
100
+
101
+ ### Framework examples
102
+
103
+ ```erb
104
+ <%# Rails %>
105
+ <%= tag.div class: Clsx['foo', 'baz', 'is-active': @active] do %>
106
+ Hello, world!
107
+ <% end %>
108
+ ```
109
+
110
+ ```ruby
111
+ # Sinatra
112
+ erb :"<div class='#{Clsx['nav', active: @active]}'>...</div>"
113
+ ```
114
+
115
+ ## Differences from JavaScript clsx
116
+
117
+ 1. **Falsy values** — In Ruby only `false` and `nil` are falsy, so `0`, `''`, `[]`, `{}` are all truthy:
118
+ ```ruby
119
+ Clsx['foo' => 0, bar: []] # => 'foo bar'
120
+ ```
121
+
122
+ 2. **Complex hash keys** — Any valid `clsx` input works as a hash key:
123
+ ```ruby
124
+ Clsx[[{ foo: true }, 'bar'] => true] # => 'foo bar'
125
+ ```
126
+
127
+ 3. **Ignored values** — Boolean `true` and `Proc`/lambda objects are silently ignored:
128
+ ```ruby
129
+ Clsx['', proc {}, -> {}, nil, false, true] # => nil
130
+ ```
131
+
132
+ 4. **Returns `nil`** when no classes apply (not an empty string). This prevents rendering empty `class=""` attributes in template engines that skip `nil`:
133
+ ```ruby
134
+ Clsx[nil, false] # => nil
135
+ ```
136
+
137
+ 5. **Deduplication** — Duplicate classes are automatically removed:
138
+ ```ruby
139
+ Clsx['foo', 'foo'] # => 'foo'
140
+ ```
141
+
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`.
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`
153
+
154
+ ## Contributing
155
+
156
+ Bug reports and pull requests are welcome on GitHub at https://github.com/svyatov/clsx-ruby.
157
+
158
+ ## License
159
+
160
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ module Clsx
5
+ # :nodoc:
6
+ module Helper
7
+ # The clsx function can take any number of arguments,
8
+ # each of which can be Hash, Array, Boolean, String, or Symbol.
9
+ #
10
+ # **Important**
11
+ # Any falsy values are discarded! Standalone Boolean values are discarded as well.
12
+ #
13
+ # @param [Mixed] args
14
+ #
15
+ # @return [String] the joined class names
16
+ #
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
33
+ def clsx(*args)
34
+ 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
+ end
53
+
54
+ seen = {}
55
+ clsx_process(args, seen)
56
+ seen.empty? ? nil : seen.keys.join(' ')
57
+ end
58
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
59
+
60
+ alias cn clsx
61
+
62
+ private
63
+
64
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
65
+ def clsx_simple_hash(hash)
66
+ return nil if hash.empty?
67
+
68
+ seen = {}
69
+ hash.each do |key, value|
70
+ next unless value
71
+
72
+ klass = key.class
73
+
74
+ if klass == Symbol
75
+ seen[key.name] = true
76
+ elsif klass == String
77
+ seen[key] = true unless key.empty?
78
+ else
79
+ # Complex key - fall back to full processing
80
+ seen = {}
81
+ clsx_process([hash], seen)
82
+ return seen.empty? ? nil : seen.keys.join(' ')
83
+ end
84
+ end
85
+
86
+ seen.empty? ? nil : seen.keys.join(' ')
87
+ end
88
+
89
+ # rubocop:disable Style/MultipleComparison
90
+ def clsx_process(args, seen)
91
+ deferred = nil
92
+
93
+ args.each do |arg|
94
+ klass = arg.class
95
+
96
+ if klass == String
97
+ seen[arg] = true unless arg.empty? || seen.key?(arg)
98
+ elsif klass == Symbol
99
+ str = arg.name
100
+ seen[str] = true unless seen.key?(str)
101
+ elsif klass == Array
102
+ clsx_process(arg, seen)
103
+ elsif klass == Hash
104
+ arg.each { |key, value| (deferred ||= []) << key if value }
105
+ elsif klass == Integer || klass == Float
106
+ str = arg.to_s
107
+ seen[str] = true unless seen.key?(str)
108
+ elsif klass == NilClass || klass == FalseClass || klass == TrueClass || klass == Proc
109
+ next
110
+ else
111
+ str = arg.to_s
112
+ seen[str] = true unless str.empty? || seen.key?(str)
113
+ end
114
+ end
115
+
116
+ clsx_process(deferred, seen) if deferred
117
+ end
118
+ # rubocop:enable Style/MultipleComparison, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clsx
4
+ VERSION = '1.0.0'
5
+ end
data/lib/clsx.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'clsx/version'
4
+ require_relative 'clsx/helper'
5
+
6
+ # :nodoc:
7
+ module Clsx
8
+ extend Helper
9
+
10
+ def self.[](*)
11
+ clsx(*)
12
+ end
13
+ end
14
+
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
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clsx-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Leonid Svyatov
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A tiny utility for constructing CSS class strings conditionally
13
+ email:
14
+ - leonid@svyatov.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - CHANGELOG.md
20
+ - CLAUDE.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - lib/clsx.rb
24
+ - lib/clsx/helper.rb
25
+ - lib/clsx/version.rb
26
+ homepage: https://github.com/svyatov/clsx-ruby
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ source_code_uri: https://github.com/svyatov/clsx-ruby
31
+ changelog_uri: https://github.com/svyatov/clsx-ruby/blob/main/CHANGELOG.md
32
+ rubygems_mfa_required: 'true'
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.2.0
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 4.0.4
48
+ specification_version: 4
49
+ summary: clsx / classnames for Ruby
50
+ test_files: []