tail_merge 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff6ca50c862b6942ab07296b4111bc84928f960883f939e153069b926b51dc38
4
- data.tar.gz: 853009b24fb888e2d0af540c274541fdebbad87aded06fbab79cfcb1061f469a
3
+ metadata.gz: df9120474172d53dc79d7592c20832032545e05572a437c63b7d342cfd2bb33a
4
+ data.tar.gz: 24e6b1bc37558804a3a4fa4afdd06b4a4d21e5ace5ed349def07a5d0b2768584
5
5
  SHA512:
6
- metadata.gz: dc1d28edcf9b35ea2b98ba0a42821d932ad8109c21e818fa6b499fc8cdbca2db4dbe8c54540fb4d8c79dca2523f90a5883398e8736e4ef9f7693e855c956369c
7
- data.tar.gz: 385cea35eb5451eb138557b3ec3312834fc4826449802f16f7f40531922ca38dc9fdd55c829fcdcc819820b7196cd10a26095ae65b5f252fc3c830d9eb380fdd
6
+ metadata.gz: 904e825d0f09aace9c811225fa59c455eb473d958843b587be89e91dcf2d080ac373c73147699ed3f93ebfed28f9570e2e58025d95eb48ebce4c92e889c25e53
7
+ data.tar.gz: 83604251ac57755a2106dfab3544379d8f234fffe3440599ea03a2c217085aa1b9dff583ba3a6041df9e2bffc72713f89aaaf6273c4942d1c1838ee0e622464e
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
@@ -102,6 +102,48 @@ TailMerge.merge "px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]"
102
102
  => "hover:bg-dark-red p-3 bg-[#B91C1C]"
103
103
  ```
104
104
 
105
+ ## More speed?
106
+
107
+ 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.
108
+
109
+ ```ruby
110
+ tail_merge_instance = TailMerge.new
111
+ tail_merge_instance.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
112
+ => "hover:bg-dark-red p-3 bg-[#B91C1C]" # Write to cache, still fast though
113
+
114
+ # Second time, same key, read from cache
115
+ tail_merge_instance.merge %w[px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]]
116
+ => "hover:bg-dark-red p-3 bg-[#B91C1C]" # Read from cache, much faster!
117
+
118
+ # Third time, string key instead of array, read from same cache
119
+ tail_merge_instance.merge "px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]"
120
+ => "hover:bg-dark-red p-3 bg-[#B91C1C]" # Read from cache, much faster!
121
+ ```
122
+
123
+ This caching technique was inspired by [[Tailwind Merge](https://github.com/dcastil/tailwind-merge)](https://github.com/gjtorikian/tailwind_merge).
124
+
125
+ ## Benchmark
126
+
127
+ So how fast/much faster is TailMerge?
128
+
129
+ I've benchmarked TailMerge with an instance (cached) and without an instance against Tailwind Merge with (cached) and without a merger instance, and these are the results:
130
+
131
+ ```
132
+ user system total real
133
+ Rust: TailMerge.merge (all samples): 0.216178 0.001744 0.217922 ( 0.219441)
134
+ Rust: Cached TailMerge.merge (all samples): 0.005465 0.000092 0.005557 ( 0.005581)
135
+ Ruby: TailwindMerge each time (all samples): 50.391383 0.494058 50.885441 ( 52.272354)
136
+ Ruby:Cached TailwindMerge (all samples): 0.011672 0.000140 0.011812 ( 0.011813)
137
+ ```
138
+
139
+ As you can see TailMerge is much faster using pure Ruby to merge classes.
140
+
141
+ The benchmark loops through an array of strings and arrays and merges them 1000 times.
142
+
143
+ The difference between the cached runs, obviously, is much smaller as we are basically benchmarking the cache lookup and not the actual merge.
144
+
145
+ In reality we will not deal with 1000 merges to be done per page and I suspect you'd be much closer to the non-cached runs.
146
+
105
147
  ## Contributing
106
148
 
107
- Bug reports and pull requests are welcome on GitHub at https://github.com/abuisman/tail_merge.
149
+ 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.
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,98 @@
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
+ # Normalize all samples to strings
60
+ normalized_samples = samples.map { |classes| classes.is_a?(Array) ? classes.join(' ') : classes }
61
+
62
+ puts "Benchmarking class merging strategies (whole set)..."
63
+ puts "-" * 50
64
+ puts
65
+
66
+ Benchmark.bm(30) do |x|
67
+ x.report("Rust: TailMerge.merge (all samples):") do
68
+ 1000.times do
69
+ normalized_samples.each do |classes|
70
+ TailMerge.merge(classes)
71
+ end
72
+ end
73
+ end
74
+
75
+ x.report("Rust: Cached TailMerge.merge (all samples):") do
76
+ 1000.times do
77
+ normalized_samples.each do |classes|
78
+ tail_merge_instance.merge(classes)
79
+ end
80
+ end
81
+ end
82
+
83
+ x.report("Ruby: TailwindMerge each time (all samples):") do
84
+ 1000.times do
85
+ normalized_samples.each do |classes|
86
+ TailwindMerge::Merger.new.merge(classes)
87
+ end
88
+ end
89
+ end
90
+
91
+ x.report("Ruby:Cached TailwindMerge (all samples):") do
92
+ 1000.times do
93
+ normalized_samples.each do |classes|
94
+ cached_merger.merge(classes)
95
+ end
96
+ end
97
+ end
98
+ 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")
@@ -14,6 +14,7 @@ fn merge_tailwind_classes(args: &[Value]) -> Result<RString, Error> {
14
14
 
15
15
  // ---------- 2. collect class tokens ------------------------------------
16
16
  let mut tokens = Vec::<String>::new();
17
+ let is_string_input = matches!(args[0].clone().try_convert::<RString>(), Ok(_));
17
18
  match args[0].clone().try_convert::<RString>() {
18
19
  Ok(rstr) => tokens.extend(rstr.to_string()?.split_whitespace().map(str::to_owned)),
19
20
  Err(_) => {
@@ -25,6 +26,24 @@ fn merge_tailwind_classes(args: &[Value]) -> Result<RString, Error> {
25
26
  }
26
27
  }
27
28
 
29
+ // Early returns for simple cases
30
+ if is_string_input {
31
+ let rstr: RString = args[0].clone().try_convert()?;
32
+ let s = rstr.to_string()?;
33
+ if !s.contains(' ') {
34
+ // Single class string, return as-is
35
+ return Ok(RString::new(&s));
36
+ }
37
+ } else {
38
+ // Array input
39
+ if tokens.is_empty() {
40
+ return Ok(RString::new(""));
41
+ }
42
+ if tokens.len() == 1 {
43
+ return Ok(RString::new(&tokens[0]));
44
+ }
45
+ }
46
+
28
47
  // ---------- 3. extract options -----------------------------------------
29
48
  let mut prefix: Option<String> = None;
30
49
  let mut separator: Option<String> = None;
@@ -100,8 +119,10 @@ fn merge_tailwind_classes(args: &[Value]) -> Result<RString, Error> {
100
119
 
101
120
  #[magnus::init]
102
121
  fn init() -> Result<(), Error> {
103
- let module = define_module("TailMerge")?;
104
- // -1 = variable arity (positional + kw-hash)
105
- 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))?;
106
127
  Ok(())
107
128
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TailMerge
4
- VERSION = "0.2.0"
3
+ class TailMerge
4
+ VERSION = "0.4.0"
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.2.0
4
+ version: 0.4.0
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