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 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
@@ -0,0 +1,14 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
9
+
10
+ Metrics/BlockLength:
11
+ Enabled: false
12
+
13
+ Layout/LineLength:
14
+ Enabled: false
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TailMerge
4
+ VERSION = "0.4.2"
5
+ end
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
@@ -0,0 +1,4 @@
1
+ module TailMerge
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []