tail_merge 0.4.2-arm64-darwin
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 +7 -0
- data/.rubocop.yml +14 -0
- data/README.md +151 -0
- data/Rakefile +52 -0
- data/benchmark/benchmark.rb +111 -0
- data/lib/tail_merge/3.1/merger.bundle +0 -0
- data/lib/tail_merge/3.2/merger.bundle +0 -0
- data/lib/tail_merge/3.3/merger.bundle +0 -0
- data/lib/tail_merge/3.4/merger.bundle +0 -0
- data/lib/tail_merge/version.rb +5 -0
- data/lib/tail_merge.rb +27 -0
- data/sig/tail_merge.rbs +4 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e984ca5df1f1324bfe0911c53c9f111130d8aa62b91ac7035e1a1a85959c57d6
|
4
|
+
data.tar.gz: ef0d179ddeb7008fea6cddbdff8264c4bc4c4b50197d3e050a2ee6d773c99116
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 34df6ede5531c36f325c672a5ea6e09ce8cc4d329289480e2d4cdb363aaaf09a1e19187a11520ed77a1262e9cda3991b785222800604c88fabe83d3b1b5e6aca
|
7
|
+
data.tar.gz: 206fb5aade2decf1684d5836a6972ac1145f2f2590df57743a1711fb5fa1d0cea9914e67812f3415f36f9d9b3aaf9eb35ed5accc20b04faa58c00e33298571ca
|
data/.rubocop.yml
ADDED
data/README.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# TailMerge
|
2
|
+
|
3
|
+
TailMerge is a super-fast utility library to merge Tailwind CSS classes without conflicts.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
TailMerge.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
|
7
|
+
=> "hover:bg-dark-red p-3 bg-[#B91C1C]"
|
8
|
+
```
|
9
|
+
|
10
|
+
Classes that appear later in the list override earlier ones.
|
11
|
+
|
12
|
+
By leveraging the Rust crate [rustui_merge](https://docs.rs/rustui_merge/latest/rustui_merge/), TailMerge merges classes significantly faster than pure Ruby alternatives.
|
13
|
+
|
14
|
+
## Purpose
|
15
|
+
|
16
|
+
When you use Tailwind CSS to style components, you'll often want to adjust the styling of a component in certain situations.
|
17
|
+
|
18
|
+
An example:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class Well < ApplicationComponent
|
22
|
+
def initialize(**options)
|
23
|
+
@classes = options.delete(:classes)
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
tag.div class: default_classes + @classes do
|
28
|
+
content
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_classes
|
33
|
+
%w[bg-gray-100 rounded-lg p-4]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
If you want to render this component somewhere with a different background, ideally you'd be able to do this:
|
39
|
+
|
40
|
+
```erb
|
41
|
+
<%= render Well.new(classes: %w[bg-blue-50 p-2]) do %>
|
42
|
+
<p>Hello</p>
|
43
|
+
<% end %>
|
44
|
+
```
|
45
|
+
|
46
|
+
Sadly, this will not work. The div will have a gray-100 background and a padding of 4 instead of the intended blue-50 and p-2.
|
47
|
+
|
48
|
+
This is where TailMerge comes in. It allows you to merge classes without conflicts.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
TailMerge.merge %w[bg-gray-100 rounded-lg p-4] + %w[bg-blue-50 p-2]
|
52
|
+
=> "rounded-lg bg-blue-50 p-2"
|
53
|
+
```
|
54
|
+
|
55
|
+
Implementing this in your component is easy:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Well < ApplicationComponent
|
59
|
+
def initialize(**options)
|
60
|
+
@classes = options.delete(:classes)
|
61
|
+
end
|
62
|
+
|
63
|
+
def call
|
64
|
+
tag.div class: TailMerge.merge(default_classes + @classes) do
|
65
|
+
content
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_classes
|
70
|
+
%w[bg-gray-100 rounded-lg p-4]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
No more conflicts!
|
76
|
+
|
77
|
+
## Installation
|
78
|
+
|
79
|
+
Add the gem to your Gemfile:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
gem "tail_merge"
|
83
|
+
```
|
84
|
+
|
85
|
+
Run `bundle install` to install the gem.
|
86
|
+
|
87
|
+
## Usage
|
88
|
+
|
89
|
+
You can pass either a string or an array of strings to the merge method. Values passed later override previous ones. The result is always a string, ready for use in ERB templates.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
TailMerge.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
|
93
|
+
=> "hover:bg-dark-red p-3 bg-[#B91C1C]"
|
94
|
+
```
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
TailMerge.merge "px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]"
|
98
|
+
=> "hover:bg-dark-red p-3 bg-[#B91C1C]"
|
99
|
+
```
|
100
|
+
|
101
|
+
## More speed?
|
102
|
+
|
103
|
+
You can create an instance of TailMerge and call `merge` on it instead of on the `TailMerge` class. This will cache the results of the merge.
|
104
|
+
|
105
|
+
This is useful in cases where you need to merge the same classes repeatedly, such as when rendering a list of the same component.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
tail_merge_instance = TailMerge.new
|
109
|
+
tail_merge_instance.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
|
110
|
+
=> "hover:bg-dark-red p-3 bg-[#B91C1C]" # Write to cache, still fast though
|
111
|
+
|
112
|
+
# Second time, same key, read from cache
|
113
|
+
tail_merge_instance.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
|
114
|
+
=> "hover:bg-dark-red p-3 bg-[#B91C1C]" # Read from cache, much faster!
|
115
|
+
|
116
|
+
# Third time, string key instead of array, read from same cache
|
117
|
+
tail_merge_instance.merge "px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]"
|
118
|
+
=> "hover:bg-dark-red p-3 bg-[#B91C1C]" # Read from cache, much faster!
|
119
|
+
```
|
120
|
+
|
121
|
+
This caching technique was inspired by [Tailwind Merge](https://github.com/dcastil/tailwind-merge).
|
122
|
+
|
123
|
+
## Benchmark
|
124
|
+
|
125
|
+
So how fast is TailMerge?
|
126
|
+
|
127
|
+
I've benchmarked TailMerge with and without caching, and compared it to Tailwind Merge (also with and without caching). Here are the results:
|
128
|
+
|
129
|
+
```
|
130
|
+
user system total real
|
131
|
+
Rust: TailMerge.merge (all samples): 0.371744 0.019642 0.391386 ( 0.391821)
|
132
|
+
Rust: Cached TailMerge.merge (all samples): 0.012976 0.000580 0.013556 ( 0.013560)
|
133
|
+
Ruby: TailwindMerge each time (all samples): 51.488919 0.225130 51.714049 ( 51.883713)
|
134
|
+
Ruby:Cached TailwindMerge (all samples): 0.019882 0.000166 0.020048 ( 0.020051)
|
135
|
+
```
|
136
|
+
|
137
|
+
As you can see, TailMerge is much faster than using pure Ruby to merge classes.
|
138
|
+
|
139
|
+
The benchmark loops through an array of strings and arrays and merges them 1000 times.
|
140
|
+
|
141
|
+
The difference between the cached runs, obviously, is much smaller as we are basically benchmarking the cache lookup and not the actual merge.
|
142
|
+
|
143
|
+
In reality, you will not need to perform 1000 merges per page, and I suspect you'll be much closer to the non-cached runs.
|
144
|
+
|
145
|
+
## Contributing
|
146
|
+
|
147
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/abuisman/tail_merge . Merging will be done at my own pace and discretion.
|
148
|
+
|
149
|
+
## License
|
150
|
+
|
151
|
+
This gem is available as open source under under the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "minitest/test_task"
|
5
|
+
|
6
|
+
Minitest::TestTask.create
|
7
|
+
|
8
|
+
begin
|
9
|
+
require "rubocop/rake_task"
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
rescue LoadError
|
12
|
+
# RuboCop is an optional dev dependency; skip in minimal build environments
|
13
|
+
end
|
14
|
+
|
15
|
+
require "rb_sys/extensiontask"
|
16
|
+
|
17
|
+
task build: :compile
|
18
|
+
|
19
|
+
GEMSPEC = Gem::Specification.load("tail_merge.gemspec")
|
20
|
+
|
21
|
+
# Limit cross-compiled Ruby versions to those supported by this gem and dependencies
|
22
|
+
# Format matches rake-compiler's RUBY_CC_VERSION (colon-separated list)
|
23
|
+
ENV["RUBY_CC_VERSION"] ||= "3.1.6:3.2.6:3.3.7:3.4.1"
|
24
|
+
|
25
|
+
PLATFORMS = %w[
|
26
|
+
aarch64-linux-gnu
|
27
|
+
aarch64-linux-musl
|
28
|
+
arm-linux-gnu
|
29
|
+
arm-linux-musl
|
30
|
+
arm64-darwin
|
31
|
+
x64-mingw-ucrt
|
32
|
+
x64-mingw32
|
33
|
+
x86-linux-gnu
|
34
|
+
x86-linux-musl
|
35
|
+
x86-mingw32
|
36
|
+
x86_64-darwin
|
37
|
+
x86_64-linux-gnu
|
38
|
+
x86_64-linux-musl
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
RbSys::ExtensionTask.new("merger", GEMSPEC) do |ext|
|
42
|
+
ext.lib_dir = "lib/tail_merge"
|
43
|
+
ext.cross_compile = true
|
44
|
+
ext.cross_platform = %w[x86-mingw32 x64-mingw-ucrt x64-mingw32 x86-linux x86_64-linux x86_64-darwin arm64-darwin]
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Build native extension for a given platform (i.e. `rake 'native[x86_64-linux]'`)"
|
48
|
+
task :native, [:platform] do |_t, platform:|
|
49
|
+
sh "bundle", "exec", "rb-sys-dock", "--platform", platform, "--build"
|
50
|
+
end
|
51
|
+
|
52
|
+
task default: %i[compile test rubocop]
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "benchmark"
|
4
|
+
require "tailwind_merge"
|
5
|
+
|
6
|
+
require "bundler/setup"
|
7
|
+
require "tail_merge"
|
8
|
+
|
9
|
+
samples = [
|
10
|
+
["relative"],
|
11
|
+
["self-center"],
|
12
|
+
["self-start"],
|
13
|
+
%w[shadow-inner size-4 text-xs],
|
14
|
+
%w[shadow-inner size-7],
|
15
|
+
["size-10"],
|
16
|
+
["static"],
|
17
|
+
["upload-attachment", "flex-none", "rounded-3xl", "min-h-16", "group", "relative"],
|
18
|
+
%w[w-full py-2 px-2 rounded-md w-44],
|
19
|
+
"relative",
|
20
|
+
"self-center",
|
21
|
+
"self-start",
|
22
|
+
"shadow-inner size-4 text-xs",
|
23
|
+
"shadow-inner size-7",
|
24
|
+
"size-10",
|
25
|
+
"static",
|
26
|
+
"upload-attachment flex-none rounded-3xl min-h-16 group relative",
|
27
|
+
"w-full py-2 px-2 rounded-md w-44",
|
28
|
+
["p-4", "px-2", "py-6", "m-3", "mx-8", "my-2", "bg-blue-500", "bg-red-600", "text-sm", "text-lg", "font-bold",
|
29
|
+
"font-normal", "rounded-lg", "rounded-xl", "shadow-md", "shadow-lg", "hover:bg-blue-700", "hover:bg-red-800", "focus:ring-2", "focus:ring-4"],
|
30
|
+
%w[grid flex inline-flex grid-cols-3 grid-cols-4 gap-2 gap-4 items-center items-start
|
31
|
+
justify-between justify-center p-8 p-4 bg-gray-100 bg-white border border-2 rounded-full rounded-md],
|
32
|
+
["transform", "scale-100", "scale-110", "rotate-45", "rotate-90", "translate-x-2", "translate-x-4", "skew-y-3",
|
33
|
+
"skew-y-6", "transition", "duration-200", "duration-500", "ease-in", "ease-out", "delay-150", "delay-300"],
|
34
|
+
["w-full", "w-1/2", "w-3/4", "h-screen", "h-full", "h-32", "min-h-0", "min-h-full", "max-w-xs", "max-w-xl",
|
35
|
+
"overflow-hidden", "overflow-scroll", "object-cover", "object-contain", "opacity-75", "opacity-100"],
|
36
|
+
%w[text-left text-center text-right text-justify tracking-wide tracking-wider leading-tight
|
37
|
+
leading-loose uppercase lowercase capitalize normal-case truncate line-clamp-2 line-clamp-3],
|
38
|
+
%w[border-t border-b border-l border-r border-solid border-dashed border-red-500
|
39
|
+
border-blue-600 divide-y divide-x divide-gray-200 divide-blue-300 ring-2 ring-4 ring-offset-2],
|
40
|
+
%w[cursor-pointer cursor-wait select-none select-text resize resize-none z-10 z-50
|
41
|
+
float-left float-right clear-both clear-none box-border box-content],
|
42
|
+
%w[bg-opacity-50 bg-opacity-75 backdrop-blur-sm backdrop-blur-lg backdrop-filter filter
|
43
|
+
brightness-90 brightness-110 contrast-75 contrast-125 saturate-50 saturate-200],
|
44
|
+
["focus:outline-none", "focus:ring-2", "focus:ring-offset-2", "focus:border-blue-500", "hover:scale-105",
|
45
|
+
"hover:rotate-3", "active:scale-95", "disabled:opacity-50", "disabled:cursor-not-allowed"],
|
46
|
+
["sm:text-lg", "md:text-xl", "lg:text-2xl", "xl:text-3xl", "2xl:text-4xl", "sm:w-1/2", "md:w-2/3", "lg:w-3/4",
|
47
|
+
"xl:w-full", "2xl:max-w-screen-xl", "sm:p-4", "md:p-6", "lg:p-8", "xl:p-10"],
|
48
|
+
["dark:bg-gray-800", "dark:text-white", "dark:border-gray-600", "dark:hover:bg-gray-700", "dark:focus:ring-blue-800",
|
49
|
+
"bg-white", "text-black", "border-gray-200", "hover:bg-gray-100"],
|
50
|
+
["group-hover:scale-110", "group-hover:rotate-6", "group-focus:outline-none", "group-active:scale-95",
|
51
|
+
"peer-checked:bg-blue-500", "peer-checked:text-white", "peer-disabled:opacity-50"],
|
52
|
+
["animate-spin", "animate-pulse", "animate-bounce", "animate-ping", "motion-safe:animate-spin",
|
53
|
+
"motion-reduce:animate-none", "transition-all", "duration-300", "ease-in-out", "delay-150"],
|
54
|
+
["space-x-4", "space-x-reverse", "space-y-6", "space-y-reverse", "gap-x-4", "gap-y-6", "place-items-center",
|
55
|
+
"place-content-center", "place-self-center", "content-center"],
|
56
|
+
%w[from-blue-500 to-purple-500 via-pink-500 bg-gradient-to-r bg-gradient-to-br text-transparent
|
57
|
+
bg-clip-text bg-origin-border bg-no-repeat bg-cover],
|
58
|
+
%w[columns-2 columns-3 break-inside-avoid break-after-column aspect-square aspect-video
|
59
|
+
object-right-top object-left-bottom isolation-auto mix-blend-multiply],
|
60
|
+
["first:pt-0", "last:pb-0", "odd:bg-gray-50", "even:bg-white", "first-letter:text-7xl", "first-line:uppercase",
|
61
|
+
"selection:bg-yellow-200", "selection:text-black"],
|
62
|
+
["[mask-type:luminance]", "[mask-type:alpha]", "[transform-style:preserve-3d]", "[clip-path:circle(50%)]",
|
63
|
+
"[-webkit-text-stroke:2px]", "[text-align-last:justify]"],
|
64
|
+
%w[will-change-scroll will-change-transform scroll-smooth scroll-mt-2 scroll-pb-4 overscroll-contain
|
65
|
+
touch-pan-right touch-manipulation],
|
66
|
+
%w[hyphens-auto hyphens-manual text-underline-offset-2 text-decoration-thickness-2 indent-8 indent-16
|
67
|
+
vertical-align-sub vertical-align-super]
|
68
|
+
]
|
69
|
+
|
70
|
+
# Pre-initialize cached mergers
|
71
|
+
cached_merger = TailwindMerge::Merger.new
|
72
|
+
|
73
|
+
tail_merge_instance = TailMerge.new
|
74
|
+
|
75
|
+
puts "Benchmarking class merging strategies (whole set)..."
|
76
|
+
puts "-" * 50
|
77
|
+
puts
|
78
|
+
|
79
|
+
Benchmark.bm(30) do |x|
|
80
|
+
x.report("Rust: TailMerge.merge (all samples):") do
|
81
|
+
1000.times do
|
82
|
+
samples.each do |classes|
|
83
|
+
TailMerge.merge(classes)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
x.report("Rust: Cached TailMerge.merge (all samples):") do
|
89
|
+
1000.times do
|
90
|
+
samples.each do |classes|
|
91
|
+
tail_merge_instance.merge(classes)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
x.report("Ruby: TailwindMerge each time (all samples):") do
|
97
|
+
1000.times do
|
98
|
+
samples.each do |classes|
|
99
|
+
TailwindMerge::Merger.new.merge(classes)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
x.report("Ruby:Cached TailwindMerge (all samples):") do
|
105
|
+
1000.times do
|
106
|
+
samples.each do |classes|
|
107
|
+
cached_merger.merge(classes)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/tail_merge.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tail_merge/version"
|
4
|
+
require_relative "tail_merge/merger"
|
5
|
+
|
6
|
+
# Main class for merging tailwind classes.
|
7
|
+
class TailMerge
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
def self.merge(classes, options = {})
|
11
|
+
Merger.perform(classes, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
@options = options
|
18
|
+
@class_hash = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge(classes)
|
22
|
+
return "" if classes.empty?
|
23
|
+
|
24
|
+
classes = classes.join(" ") if classes.is_a?(Array)
|
25
|
+
@class_hash[classes] ||= Merger.perform(classes, options)
|
26
|
+
end
|
27
|
+
end
|
data/sig/tail_merge.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tail_merge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.2
|
5
|
+
platform: arm64-darwin
|
6
|
+
authors:
|
7
|
+
- Achilleas Buisman
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-20 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Merge Tailwind CSS classes
|
14
|
+
email:
|
15
|
+
- accounts@abuisman.nl
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rubocop.yml"
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- benchmark/benchmark.rb
|
24
|
+
- lib/tail_merge.rb
|
25
|
+
- lib/tail_merge/3.1/merger.bundle
|
26
|
+
- lib/tail_merge/3.2/merger.bundle
|
27
|
+
- lib/tail_merge/3.3/merger.bundle
|
28
|
+
- lib/tail_merge/3.4/merger.bundle
|
29
|
+
- lib/tail_merge/version.rb
|
30
|
+
- sig/tail_merge.rbs
|
31
|
+
homepage: https://github.com/abuisman/tail_merge
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata:
|
35
|
+
allowed_push_host: https://rubygems.org
|
36
|
+
homepage_uri: https://github.com/abuisman/tail_merge
|
37
|
+
source_code_uri: https://github.com/abuisman/tail_merge
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.1'
|
47
|
+
- - "<"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 3.5.dev
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.3.11
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.5.23
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Merge Tailwind CSS classes
|
60
|
+
test_files: []
|