tail_merge 0.3.0 → 0.4.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: '09136ec0a7747e58715fc95aae08f3c81599247202a152bc3b21c85217b5dbad'
4
- data.tar.gz: b4313ed6f949a48149d1e417e88c84bd026dd9791197f68f0137c2217e5798fb
3
+ metadata.gz: 163e2f04ba6092bb1d8a0e268dab58a087c9f853edc52ba1ae091cc6b2c7ece4
4
+ data.tar.gz: e0aa6a361b8e4219a783b6a00b099ca8dfe6b8c82ae071b2cac8ea4f79d268ce
5
5
  SHA512:
6
- metadata.gz: 8a797eafd289cc37273f8ebf323241f6245f9a90337dc2c9b9841d279ec7eac957b492ed974b4a4c9bc824485dd057722fc2191ff04d9bddeff6ec3b50b85f43
7
- data.tar.gz: 7e52a4c5a9b2d763228aab6930895742a2f2dddfbbb2581fb6d8b31dfd719bcb85b75d86a43976f45b65d22dadd6527187a771744a432d808fe24c241c11694a
6
+ metadata.gz: f5071ee3b5bccf88d692a1863e2d7f4a1c37fd00f13786dd685127a557b5c23f064ece27f8e99f2719aae2e989b52ff09c1cc2395fc7977ceb2a8f7e1704193d
7
+ data.tar.gz: 8debb6513dc3035e7acab2ee0ebd58cb4ff265d523c28a17dc77d64fb7930faa02c91c8371875307525c6c63b50080d86f0c1ef20679be7a73077b2d6b76716d
data/Cargo.lock CHANGED
@@ -141,6 +141,14 @@ version = "2.7.4"
141
141
  source = "registry+https://github.com/rust-lang/crates.io-index"
142
142
  checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
143
143
 
144
+ [[package]]
145
+ name = "merger"
146
+ version = "0.1.0"
147
+ dependencies = [
148
+ "magnus",
149
+ "rustui_merge",
150
+ ]
151
+
144
152
  [[package]]
145
153
  name = "minimal-lexical"
146
154
  version = "0.2.1"
@@ -278,14 +286,6 @@ dependencies = [
278
286
  "unicode-ident",
279
287
  ]
280
288
 
281
- [[package]]
282
- name = "tail_merge"
283
- version = "0.1.0"
284
- dependencies = [
285
- "magnus",
286
- "rustui_merge",
287
- ]
288
-
289
289
  [[package]]
290
290
  name = "unicode-ident"
291
291
  version = "1.0.18"
data/Cargo.toml CHANGED
@@ -5,3 +5,4 @@
5
5
  [workspace]
6
6
  members = ["./ext/tail_merge"]
7
7
  resolver = "2"
8
+
data/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # TailMerge
2
2
 
3
- TailMerge is a super fast utility library to merge Tailwind CSS classes without conflicts.
3
+ TailMerge is a super-fast utility library to merge Tailwind CSS classes without conflicts.
4
4
 
5
5
  ```ruby
6
6
  TailMerge.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
7
7
  => "hover:bg-dark-red p-3 bg-[#B91C1C]"
8
8
  ```
9
9
 
10
- Classes on the right will override classes on the left.
10
+ Classes that appear later in the list override earlier ones.
11
11
 
12
- TailMerge wraps the Rust library [rustui_merge](https://docs.rs/rustui_merge/latest/rustui_merge/) so that it can be used in Ruby. This makes it a lot faster than combining classes in Ruby.
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
13
 
14
14
  ## Purpose
15
15
 
16
- When you use Tailwindcss to style components, you will probably run into the situation where you want to adjust the styling of component in a specific situation.
16
+ When you use Tailwind CSS to style components, you'll often want to adjust the styling of a component in certain situations.
17
17
 
18
18
  An example:
19
19
 
@@ -24,7 +24,7 @@ class Well < ApplicationComponent
24
24
  end
25
25
 
26
26
  def call
27
- tag.div class: default_classes + @classes
27
+ tag.div class: default_classes + @classes do
28
28
  content
29
29
  end
30
30
  end
@@ -61,7 +61,7 @@ class Well < ApplicationComponent
61
61
  end
62
62
 
63
63
  def call
64
- tag.div class: TailMerge.merge(default_classes + @classes)
64
+ tag.div class: TailMerge.merge(default_classes + @classes) do
65
65
  content
66
66
  end
67
67
  end
@@ -86,11 +86,7 @@ Run `bundle install` to install the gem.
86
86
 
87
87
  ## Usage
88
88
 
89
- You can pass a string or an array of strings to the `merge` method.
90
-
91
- Whatever you pass last will override whatever you pass first.
92
-
93
- A string is returned for easy use in ERB.
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.
94
90
 
95
91
  ```ruby
96
92
  TailMerge.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
@@ -102,6 +98,54 @@ TailMerge.merge "px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]"
102
98
  => "hover:bg-dark-red p-3 bg-[#B91C1C]"
103
99
  ```
104
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
+
105
145
  ## Contributing
106
146
 
107
- Bug reports and pull requests are welcome on GitHub at https://github.com/abuisman/tail_merge.
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 CHANGED
@@ -15,7 +15,7 @@ task build: :compile
15
15
 
16
16
  GEMSPEC = Gem::Specification.load("tail_merge.gemspec")
17
17
 
18
- RbSys::ExtensionTask.new("tail_merge", GEMSPEC) do |ext|
18
+ RbSys::ExtensionTask.new("merger", GEMSPEC) do |ext|
19
19
  ext.lib_dir = "lib/tail_merge"
20
20
  end
21
21
 
@@ -0,0 +1,95 @@
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
+ ["shadow-inner", "size-4", "text-xs"],
14
+ ["shadow-inner", "size-7"],
15
+ ["size-10"],
16
+ ["static"],
17
+ ["upload-attachment", "flex-none", "rounded-3xl", "min-h-16", "group", "relative"],
18
+ ["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", "font-normal", "rounded-lg", "rounded-xl", "shadow-md", "shadow-lg", "hover:bg-blue-700", "hover:bg-red-800", "focus:ring-2", "focus:ring-4"],
29
+ ["grid", "flex", "inline-flex", "grid-cols-3", "grid-cols-4", "gap-2", "gap-4", "items-center", "items-start", "justify-between", "justify-center", "p-8", "p-4", "bg-gray-100", "bg-white", "border", "border-2", "rounded-full", "rounded-md"],
30
+ ["transform", "scale-100", "scale-110", "rotate-45", "rotate-90", "translate-x-2", "translate-x-4", "skew-y-3", "skew-y-6", "transition", "duration-200", "duration-500", "ease-in", "ease-out", "delay-150", "delay-300"],
31
+ ["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", "overflow-hidden", "overflow-scroll", "object-cover", "object-contain", "opacity-75", "opacity-100"],
32
+ ["text-left", "text-center", "text-right", "text-justify", "tracking-wide", "tracking-wider", "leading-tight", "leading-loose", "uppercase", "lowercase", "capitalize", "normal-case", "truncate", "line-clamp-2", "line-clamp-3"],
33
+ ["border-t", "border-b", "border-l", "border-r", "border-solid", "border-dashed", "border-red-500", "border-blue-600", "divide-y", "divide-x", "divide-gray-200", "divide-blue-300", "ring-2", "ring-4", "ring-offset-2"],
34
+ ["cursor-pointer", "cursor-wait", "select-none", "select-text", "resize", "resize-none", "z-10", "z-50", "float-left", "float-right", "clear-both", "clear-none", "box-border", "box-content"],
35
+ ["bg-opacity-50", "bg-opacity-75", "backdrop-blur-sm", "backdrop-blur-lg", "backdrop-filter", "filter", "brightness-90", "brightness-110", "contrast-75", "contrast-125", "saturate-50", "saturate-200"],
36
+ ["focus:outline-none", "focus:ring-2", "focus:ring-offset-2", "focus:border-blue-500", "hover:scale-105", "hover:rotate-3", "active:scale-95", "disabled:opacity-50", "disabled:cursor-not-allowed"],
37
+ ["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", "xl:w-full", "2xl:max-w-screen-xl", "sm:p-4", "md:p-6", "lg:p-8", "xl:p-10"],
38
+ ["dark:bg-gray-800", "dark:text-white", "dark:border-gray-600", "dark:hover:bg-gray-700", "dark:focus:ring-blue-800", "bg-white", "text-black", "border-gray-200", "hover:bg-gray-100"],
39
+ ["group-hover:scale-110", "group-hover:rotate-6", "group-focus:outline-none", "group-active:scale-95", "peer-checked:bg-blue-500", "peer-checked:text-white", "peer-disabled:opacity-50"],
40
+ ["animate-spin", "animate-pulse", "animate-bounce", "animate-ping", "motion-safe:animate-spin", "motion-reduce:animate-none", "transition-all", "duration-300", "ease-in-out", "delay-150"],
41
+ ["space-x-4", "space-x-reverse", "space-y-6", "space-y-reverse", "gap-x-4", "gap-y-6", "place-items-center", "place-content-center", "place-self-center", "content-center"],
42
+ ["from-blue-500", "to-purple-500", "via-pink-500", "bg-gradient-to-r", "bg-gradient-to-br", "text-transparent", "bg-clip-text", "bg-origin-border", "bg-no-repeat", "bg-cover"],
43
+ ["columns-2", "columns-3", "break-inside-avoid", "break-after-column", "aspect-square", "aspect-video", "object-right-top", "object-left-bottom", "isolation-auto", "mix-blend-multiply"],
44
+ ["first:pt-0", "last:pb-0", "odd:bg-gray-50", "even:bg-white", "first-letter:text-7xl", "first-line:uppercase", "selection:bg-yellow-200", "selection:text-black"],
45
+ ["[mask-type:luminance]", "[mask-type:alpha]", "[transform-style:preserve-3d]", "[clip-path:circle(50%)]", "[-webkit-text-stroke:2px]", "[text-align-last:justify]"],
46
+ ["will-change-scroll", "will-change-transform", "scroll-smooth", "scroll-mt-2", "scroll-pb-4", "overscroll-contain", "touch-pan-right", "touch-manipulation"],
47
+ ["hyphens-auto", "hyphens-manual", "text-underline-offset-2", "text-decoration-thickness-2", "indent-8", "indent-16", "vertical-align-sub", "vertical-align-super"]
48
+ ]
49
+
50
+ require 'benchmark'
51
+ require 'tail_merge'
52
+ require 'tailwind_merge'
53
+
54
+ # Pre-initialize cached mergers
55
+ cached_merger = TailwindMerge::Merger.new
56
+
57
+ tail_merge_instance = TailMerge.new
58
+
59
+ puts "Benchmarking class merging strategies (whole set)..."
60
+ puts "-" * 50
61
+ puts
62
+
63
+ Benchmark.bm(30) do |x|
64
+ x.report("Rust: TailMerge.merge (all samples):") do
65
+ 1000.times do
66
+ samples.each do |classes|
67
+ TailMerge.merge(classes)
68
+ end
69
+ end
70
+ end
71
+
72
+ x.report("Rust: Cached TailMerge.merge (all samples):") do
73
+ 1000.times do
74
+ samples.each do |classes|
75
+ tail_merge_instance.merge(classes)
76
+ end
77
+ end
78
+ end
79
+
80
+ x.report("Ruby: TailwindMerge each time (all samples):") do
81
+ 1000.times do
82
+ samples.each do |classes|
83
+ TailwindMerge::Merger.new.merge(classes)
84
+ end
85
+ end
86
+ end
87
+
88
+ x.report("Ruby:Cached TailwindMerge (all samples):") do
89
+ 1000.times do
90
+ samples.each do |classes|
91
+ cached_merger.merge(classes)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,11 +1,12 @@
1
1
  [package]
2
- name = "tail_merge"
2
+ name = "merger"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
5
  authors = ["Achilleas Buisman <accounts@abuisman.nl>"]
6
6
  publish = false
7
7
 
8
8
  [lib]
9
+ name = "merger"
9
10
  crate-type = ["cdylib"]
10
11
 
11
12
  [dependencies]
@@ -3,4 +3,4 @@
3
3
  require "mkmf"
4
4
  require "rb_sys/mkmf"
5
5
 
6
- create_rust_makefile("tail_merge/tail_merge")
6
+ create_rust_makefile("tail_merge/merger")
@@ -119,8 +119,10 @@ fn merge_tailwind_classes(args: &[Value]) -> Result<RString, Error> {
119
119
 
120
120
  #[magnus::init]
121
121
  fn init() -> Result<(), Error> {
122
- let module = define_module("TailMerge")?;
123
- // -1 = variable arity (positional + kw-hash)
124
- module.define_singleton_method("merge", function!(merge_tailwind_classes, -1))?;
122
+ let ruby = Ruby::get().unwrap();
123
+ let tail_merge_class: magnus::RClass = ruby.class_object().const_get("TailMerge")?;
124
+ let merger_module = tail_merge_class.define_module("Merger")?;
125
+ // Assuming merge_tailwind_classes is your target function
126
+ merger_module.define_singleton_method("perform", function!(merge_tailwind_classes, -1))?;
125
127
  Ok(())
126
128
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TailMerge
4
- VERSION = "0.3.0"
3
+ class TailMerge
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/tail_merge.rb CHANGED
@@ -1,9 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "tail_merge/version"
4
- require_relative "tail_merge/tail_merge"
4
+ require_relative "tail_merge/merger"
5
5
 
6
- module TailMerge
6
+ # Main class for merging tailwind classes.
7
+ class TailMerge
7
8
  class Error < StandardError; end
8
- # Your code goes here...
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
9
27
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tail_merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Achilleas Buisman
@@ -36,6 +36,7 @@ files:
36
36
  - Cargo.toml
37
37
  - README.md
38
38
  - Rakefile
39
+ - benchmark/benchmark.rb
39
40
  - ext/tail_merge/Cargo.toml
40
41
  - ext/tail_merge/extconf.rb
41
42
  - ext/tail_merge/src/lib.rs