tailwind_merge 0.14.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f932b04e38ec154f34a89704e726add6cc04f1e3b574dc8abe6aba954133dc8
4
- data.tar.gz: af5ef994eaeaf7f71c22394938cb53d5ea6f7d145400d302f24720103bbf2008
3
+ metadata.gz: 3334600a104bc6b8efbe365c3be28e32944468f9cb5b1255fdefd4d106dd9540
4
+ data.tar.gz: 2f8c9a9b989c3e808767516bff6b040ed0c3a092a5fc427aaacaae4904523bcc
5
5
  SHA512:
6
- metadata.gz: a11810739f13d68934146eac75e27f777417d886c7da28782f22a5e221f9222a9033a6c13927d3a7bbc19bd7f24c776984dcd76ac1f5d9d4f17f434fa1ff112d
7
- data.tar.gz: 01a2ca26ae565752c183e711972a1d77bdf4bacdb0672c3f1e38da62da1095d4e8e33f91cc253ce0413fa5e11d26b51b0691f2b99b29e1eb9de970386c669a87
6
+ metadata.gz: 7f8ad1a5e8dfc2386b81cf5f528e2b1b5ded8b9644dd942e777e3b356b0807d74397400c090b52aec68d54e93bc0bd8d672f9872795dd300b7e2b06b543701af
7
+ data.tar.gz: 33cc99d39060f7f2e6827d48f99bce9989df5771f0174eea1994133a25e05115855de5e4f138b763f7197801595c3ab8e13802291675786f782f7246429079dd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [v0.16.0] - 25-01-2025
2
+ ## What's Changed
3
+ * Fix and improve implementation by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/50
4
+
5
+
6
+ **Full Changelog**: https://github.com/gjtorikian/tailwind_merge/compare/v0.15.0...v0.16.0
7
+ # [v0.15.0] - 23-01-2025
8
+ ## What's Changed
9
+ * Improve gemspec by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/41
10
+ * Refactoring README.md by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/42
11
+ * Fix homepage uri by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/45
12
+ * Refactoring constants by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/43
13
+ * Fix changelog uri by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/46
14
+ * Uninstall lru_cache and install sin_lru_redux by @w-masahiro-ct in https://github.com/gjtorikian/tailwind_merge/pull/48
15
+ - Adds `ignore_empty_cache` option for saving memory
16
+ ## New Contributors
17
+ * @w-masahiro-ct made their first contribution in https://github.com/gjtorikian/tailwind_merge/pull/41
18
+
19
+ **Full Changelog**: https://github.com/gjtorikian/tailwind_merge/compare/v0.14.0...v0.15.0
1
20
  # [v0.14.0] - 19-12-2024
2
21
  ## What's Changed
3
22
  * Prevent cache tampering by freezing cached value by @david-uhlig in https://github.com/gjtorikian/tailwind_merge/pull/39
data/README.md CHANGED
@@ -20,7 +20,7 @@ To use it, pass in a single string:
20
20
  require "tailwind_merge"
21
21
 
22
22
  TailwindMerge::Merger.new.merge("px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]")
23
- # → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
23
+ # → "hover:bg-dark-red p-3 bg-[#B91C1C]"
24
24
  ```
25
25
 
26
26
  Or, an array of strings:
@@ -29,7 +29,7 @@ Or, an array of strings:
29
29
  require "tailwind_merge"
30
30
 
31
31
  TailwindMerge::Merger.new.merge(["px-2 py-1", "bg-red hover:bg-dark-red", "p-3 bg-[#B91C1C]"])
32
- # → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
32
+ # → "hover:bg-dark-red p-3 bg-[#B91C1C]"
33
33
  ```
34
34
 
35
35
  ## What's it for?
@@ -50,10 +50,8 @@ When the `ConfirmEmailComponent` is rendered, an input with the className `borde
50
50
  This is where `tailwind_merge` comes in:
51
51
 
52
52
  ```ruby
53
-
54
53
  @merger = TailwindMerge::Merger.new
55
- @merger.merge("border rounded px-2 py-1 p-5")
56
- # → "border rounded p-5"
54
+ @merger.merge("border rounded px-2 py-1 p-5") # → "border rounded p-5"
57
55
  ```
58
56
 
59
57
  tailwind-merge overrides conflicting classes and keeps everything else untouched. In the case of the implementation of `ConfirmEmailComponent`, the input now only renders the classes `border rounded p-5`.
@@ -67,30 +65,30 @@ tailwind-merge overrides conflicting classes and keeps everything else untouched
67
65
  ### Last conflicting class wins
68
66
 
69
67
  ```ruby
70
- @merger.merge('p-5 p-2 p-4') # → 'p-4'
68
+ @merger.merge("p-5 p-2 p-4") # → "p-4"
71
69
  ```
72
70
 
73
71
  ### Supports refinements
74
72
 
75
73
  ```ruby
76
- @merger.merge('p-3 px-5') # → 'p-3 px-5'
77
- @merger.merge('inset-x-4 right-4') # → 'inset-x-4 right-4'
74
+ @merger.merge("p-3 px-5") # → "p-3 px-5"
75
+ @merger.merge("inset-x-4 right-4") # → "inset-x-4 right-4"
78
76
  ```
79
77
 
80
78
  ### Resolves non-trivial conflicts
81
79
 
82
80
  ```ruby
83
- @merger.merge('inset-x-px -inset-1') # → '-inset-1'
84
- @merger.merge('bottom-auto inset-y-6') # → 'inset-y-6'
85
- @merger.merge('inline block') # → 'block'
81
+ @merger.merge("inset-x-px -inset-1") # → "-inset-1"
82
+ @merger.merge("bottom-auto inset-y-6") # → "inset-y-6"
83
+ @merger.merge("inline block") # → "block"
86
84
  ```
87
85
 
88
86
  ### Supports modifiers and stacked modifiers
89
87
 
90
88
  ```ruby
91
- @merger.merge('p-2 hover:p-4') # → 'p-2 hover:p-4'
92
- @merger.merge('hover:p-2 hover:p-4') # → 'hover:p-4'
93
- @merger.merge('hover:focus:p-2 focus:hover:p-4') # → 'focus:hover:p-4'
89
+ @merger.merge("p-2 hover:p-4") # → "p-2 hover:p-4"
90
+ @merger.merge("hover:p-2 hover:p-4") # → "hover:p-4"
91
+ @merger.merge("hover:focus:p-2 focus:hover:p-4") # → "focus:hover:p-4"
94
92
  ```
95
93
 
96
94
  The order of standard modifiers does not matter for tailwind-merge.
@@ -98,8 +96,8 @@ The order of standard modifiers does not matter for tailwind-merge.
98
96
  ### Supports arbitrary values
99
97
 
100
98
  ```ruby
101
- @merger.merge('bg-black bg-[color:var(--mystery-var)]') # → 'bg-[color:var(--mystery-var)]'
102
- @merger.merge('grid-cols-[1fr,auto] grid-cols-2') # → 'grid-cols-2'
99
+ @merger.merge("bg-black bg-[color:var(--mystery-var)]") # → "bg-[color:var(--mystery-var)]"
100
+ @merger.merge("grid-cols-[1fr,auto] grid-cols-2") # → "grid-cols-2"
103
101
  ```
104
102
 
105
103
  > **Warning**
@@ -112,11 +110,11 @@ The order of standard modifiers does not matter for tailwind-merge.
112
110
  ### Supports arbitrary properties
113
111
 
114
112
  ```ruby
115
- @merger.merge('[mask-type:luminance] [mask-type:alpha]') # → '[mask-type:alpha]'
116
- @merger.merge('[--scroll-offset:56px] lg:[--scroll-offset:44px]') # → '[--scroll-offset:56px] lg:[--scroll-offset:44px]'
113
+ @merger.merge("[mask-type:luminance] [mask-type:alpha]") # → "[mask-type:alpha]"
114
+ @merger.merge("[--scroll-offset:56px] lg:[--scroll-offset:44px]") # → "[--scroll-offset:56px] lg:[--scroll-offset:44px]"
117
115
 
118
116
  #Don't actually do this!
119
- @merger.merge('[padding:1rem] p-8') # → '[padding:1rem] p-8'
117
+ @merger.merge("[padding:1rem] p-8") # → "[padding:1rem] p-8"
120
118
  ```
121
119
 
122
120
  > **Warning** > `tailwind_merge` does not resolve conflicts between arbitrary properties and their matching Tailwind classes to keep the bundle size small.
@@ -124,11 +122,11 @@ The order of standard modifiers does not matter for tailwind-merge.
124
122
  ### Supports arbitrary variants
125
123
 
126
124
  ```ruby
127
- @merger.merge('[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4') # → '[&:nth-child(3)]:py-4'
128
- @merger.merge('dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4') # → 'hover:dark:[&:nth-child(3)]:py-4'
125
+ @merger.merge("[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4") # → "[&:nth-child(3)]:py-4"
126
+ @merger.merge("dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4") # → "hover:dark:[&:nth-child(3)]:py-4"
129
127
 
130
128
  # Don't actually do this!
131
- @merger.merge('[&:focus]:ring focus:ring-4') # → '[&:focus]:ring focus:ring-4'
129
+ @merger.merge("[&:focus]:ring focus:ring-4") # → "[&:focus]:ring focus:ring-4"
132
130
  ```
133
131
 
134
132
  > **Warning**
@@ -138,26 +136,26 @@ The order of standard modifiers does not matter for tailwind-merge.
138
136
  ### Supports important modifier
139
137
 
140
138
  ```ruby
141
- @merger.merge('!p-3 !p-4 p-5') # → '!p-4 p-5'
142
- @merger.merge('!right-2 !-inset-x-1') # → '!-inset-x-1'
139
+ @merger.merge("!p-3 !p-4 p-5") # → "!p-4 p-5"
140
+ @merger.merge("!right-2 !-inset-x-1") # → "!-inset-x-1"
143
141
  ```
144
142
 
145
143
  ## Supports postfix modifiers
146
144
 
147
145
  ```ts
148
- twMerge("text-sm leading-6 text-lg/7"); // → 'text-lg/7'
146
+ twMerge("text-sm leading-6 text-lg/7"); // → "text-lg/7"
149
147
  ```
150
148
 
151
149
  ### Preserves non-Tailwind classes
152
150
 
153
151
  ```ruby
154
- @merger.merge('p-5 p-2 my-non-tailwind-class p-4') # → 'my-non-tailwind-class p-4'
152
+ @merger.merge("p-5 p-2 my-non-tailwind-class p-4") # → "my-non-tailwind-class p-4"
155
153
  ```
156
154
 
157
155
  ### Supports custom colors out of the box
158
156
 
159
157
  ```ruby
160
- @merger.merge('text-red text-secret-sauce') # → 'text-secret-sauce'
158
+ @merger.merge("text-red text-secret-sauce") # → "text-secret-sauce"
161
159
  ```
162
160
 
163
161
  ## Basic usage
@@ -189,22 +187,24 @@ The `tailwind_merge` config is an object with several keys:
189
187
 
190
188
  ```ruby
191
189
  tailwind_merge_config = {
192
- # ↓ *Optional* Define how many values should be stored in cache.
193
- cache_size: 500,
194
- # ↓ *Optional* modifier separator from Tailwind config
195
- separator: ':',
196
- # ↓ *Optional* prefix from Tailwind config
197
- prefix: 'tw-',
198
- theme: {
199
- # Theme scales are defined here
200
- # This is not the theme object from your Tailwind config
201
- },
202
- class_groups: {
203
- # Class groups are defined here
204
- },
205
- conflicting_class_groups: {
206
- # Conflicts between class groups are defined here
207
- },
190
+ # ↓ *Optional* Define how many values should be stored in cache.
191
+ cache_size: 500,
192
+ # ↓ *Optional* Enable or disable caching nil values.
193
+ ignore_empty_cache: true,
194
+ # ↓ *Optional* modifier separator from Tailwind config
195
+ separator: ":",
196
+ # ↓ *Optional* prefix from Tailwind config
197
+ prefix: "tw-",
198
+ theme: {
199
+ # Theme scales are defined here
200
+ # This is not the theme object from your Tailwind config
201
+ },
202
+ class_groups: {
203
+ # Class groups are defined here
204
+ },
205
+ conflicting_class_groups: {
206
+ # Conflicts between class groups are defined here
207
+ }
208
208
  }
209
209
  ```
210
210
 
@@ -219,13 +219,13 @@ To use the custom configuration, pass it to the `TailwindMerge::Merger` initiali
219
219
  The library uses a concept of _class groups_ which is an array of Tailwind classes which all modify the same CSS property. For example, here is the position class group:
220
220
 
221
221
  ```ruby
222
- position_class_group = ['static', 'fixed', 'absolute', 'relative', 'sticky']
222
+ position_class_group = ["static", "fixed", "absolute", "relative", "sticky"]
223
223
  ```
224
224
 
225
225
  `tailwind_merge` resolves conflicts between classes in a class group and only keeps the last one passed to the merge function call:
226
226
 
227
227
  ```ruby
228
- @merger.merge('static sticky relative') # → 'relative'
228
+ @merger.merge("static sticky relative") # → "relative"
229
229
  ```
230
230
 
231
231
  Tailwind classes often share the beginning of the class name, so elements in a class group can also be an object with values of the same shape as a class group (the shape is recursive). In the object, each key is joined with all the elements in the corresponding array with a dash (`-`) in between.
@@ -233,7 +233,7 @@ Tailwind classes often share the beginning of the class name, so elements in a c
233
233
  For example, here is the overflow class group which results in the classes `overflow-auto`, `overflow-hidden`, `overflow-visible` and `overflow-scroll`.
234
234
 
235
235
  ```ruby
236
- overflow_class_group = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]
236
+ overflow_class_group = [{ overflow: ["auto", "hidden", "visible", "scroll"] }]
237
237
  ```
238
238
 
239
239
  Sometimes it isn't possible to enumerate every element in a class group. Think of a Tailwind class which allows arbitrary values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group.
@@ -242,16 +242,16 @@ For example, here is the fill class group:
242
242
 
243
243
  ```ruby
244
244
  is_arbitrary_value = (class_part: string) => /^\[.+\]$/.test(class_part)
245
- fill_class_group = [{ fill: ['current', IS_ARBITRARY_VALUE] }]
245
+ fill_class_group = [{ fill: ["current", IS_ARBITRARY_VALUE] }]
246
246
  ```
247
247
 
248
248
  Because the function is under the `fill` key, it will only get called for classes which start with `fill-`. Also, the function only gets passed the part of the class name which comes after `fill-`, this way you can use the same function in multiple class groups. `tailwind_merge` provides its own [validators](#validators), so you don't need to recreate them.
249
249
 
250
- You can use an empty string (`''`) as a class part if you want to indicate that the preceding part was the end. This is useful for defining elements which are marked as `DEFAULT` in the Tailwind config.
250
+ You can use an empty string (`""`) as a class part if you want to indicate that the preceding part was the end. This is useful for defining elements which are marked as `DEFAULT` in the Tailwind config.
251
251
 
252
252
  ```ruby
253
253
  # ↓ Resolves to filter and filter-none
254
- filter_class_group = [{ filter: ['', 'none'] }]
254
+ filter_class_group = [{ filter: ["", "none"] }]
255
255
  ```
256
256
 
257
257
  Each class group is defined under its ID in the `class_groups` object in the config. This ID is only used internally, and the only thing that matters is that it is unique among all class groups.
@@ -271,9 +271,7 @@ To summarize, `px-3` should stand in conflict with `pr-4`, but `pr-4` should not
271
271
  This is what the `conflicting_class_groups` object in the config is for. You define a key in it which is the ID of a class group which _creates_ a conflict and the value is an array of IDs of class group which _receive_ a conflict.
272
272
 
273
273
  ```ruby
274
- conflicting_class_groups = {
275
- px: ['pr', 'pl'],
276
- }
274
+ conflicting_class_groups = { px: ["pr", "pl"] }
277
275
  ```
278
276
 
279
277
  If a class group _creates_ a conflict, it means that if it appears in a class list string passed to `merge`, all preceding class groups in the string which _receive_ the conflict will be removed.
@@ -314,10 +312,10 @@ If you modified one of these theme scales in your Tailwind config, you can add a
314
312
 
315
313
  ```ruby
316
314
  merger = TailwindMerge::Merger.new(config: {
317
- theme: {
315
+ theme: {
318
316
  "spacing" => ["my-space"],
319
- "margin" => ["my-margin"],
320
- },
317
+ "margin" => ["my-margin"]
318
+ }
321
319
  })
322
320
  ```
323
321
 
@@ -16,7 +16,7 @@ module TailwindMerge
16
16
  def class_group_id(class_name)
17
17
  class_parts = class_name.split(CLASS_PART_SEPARATOR)
18
18
 
19
- # Classes like `-inset-1` produce an empty string as first classPart.
19
+ # Classes like `-inset-1` produce an empty string as first class_part.
20
20
  # Assume that classes for negative values are used correctly and remove it from class_parts.
21
21
  class_parts.shift if class_parts.first == "" && class_parts.length != 1
22
22
 
@@ -30,9 +30,10 @@ module TailwindMerge
30
30
 
31
31
  next_class_part_object = class_part_object[:next_part][current_class_part]
32
32
 
33
- class_group_from_next_class_part = next_class_part_object ? get_group_recursive(class_parts[1..-1], next_class_part_object) : nil
34
-
35
- return class_group_from_next_class_part if class_group_from_next_class_part
33
+ if next_class_part_object
34
+ class_group_from_next_class_part = get_group_recursive(class_parts.drop(1), next_class_part_object)
35
+ return class_group_from_next_class_part if class_group_from_next_class_part
36
+ end
36
37
 
37
38
  return if class_part_object[:validators].empty?
38
39
 
@@ -40,29 +41,23 @@ module TailwindMerge
40
41
 
41
42
  result = class_part_object[:validators].find do |v|
42
43
  validator = v[:validator]
43
-
44
- if from_theme?(validator)
45
- validator.call(@config)
46
- else
47
- validator.call(class_rest)
48
- end
44
+ from_theme?(validator) ? validator.call(@config) : validator.call(class_rest)
49
45
  end
50
46
 
51
- result.nil? ? result : result[:class_group_id]
47
+ result&.fetch(:class_group_id, nil)
52
48
  end
53
49
 
54
50
  def get_conflicting_class_group_ids(class_group_id, has_postfix_modifier)
55
51
  conflicts = @config[:conflicting_class_groups][class_group_id] || []
56
52
 
57
53
  if has_postfix_modifier && @config[:conflicting_class_group_modifiers][class_group_id]
58
- return [...conflicts, ...@config[:conflicting_class_group_modifiers][class_group_id]]
54
+ return [*conflicts, *@config[:conflicting_class_group_modifiers][class_group_id]]
59
55
  end
60
56
 
61
57
  conflicts
62
58
  end
63
59
 
64
60
  private def create_class_map(config)
65
- theme = config[:theme]
66
61
  prefix = config[:prefix]
67
62
  class_map = {
68
63
  next_part: {},
@@ -70,12 +65,12 @@ module TailwindMerge
70
65
  }
71
66
 
72
67
  prefixed_class_group_entries = get_prefixed_class_group_entries(
73
- config[:class_groups].map { |cg| [cg[0], cg[1]] },
68
+ config[:class_groups].map { |group_id, group_classes| [group_id, group_classes] },
74
69
  prefix,
75
70
  )
76
71
 
77
- prefixed_class_group_entries.each do |(class_group_id, class_group)|
78
- process_classes_recursively(class_group, class_map, class_group_id, theme)
72
+ prefixed_class_group_entries.each do |class_group_id, class_group|
73
+ process_classes_recursively(class_group, class_map, class_group_id)
79
74
  end
80
75
 
81
76
  class_map
@@ -84,53 +79,43 @@ module TailwindMerge
84
79
  private def get_prefixed_class_group_entries(class_group_entries, prefix)
85
80
  return class_group_entries if prefix.nil?
86
81
 
87
- class_group_entries.map do |(class_group_id, class_group)|
82
+ class_group_entries.map do |class_group_id, class_group|
88
83
  prefixed_class_group = class_group.map do |class_definition|
89
- next("#{prefix}#{class_definition}") if class_definition.is_a?(String)
90
-
91
- next(class_definition.transform_keys { |key| "#{prefix}#{key}" }) if class_definition.is_a?(Hash)
92
-
93
- class_definition
84
+ if class_definition.is_a?(String)
85
+ "#{prefix}#{class_definition}"
86
+ elsif class_definition.is_a?(Hash)
87
+ class_definition.transform_keys { |key| "#{prefix}#{key}" }
88
+ else
89
+ class_definition
90
+ end
94
91
  end
95
92
 
96
93
  [class_group_id, prefixed_class_group]
97
94
  end
98
95
  end
99
96
 
100
- private def process_classes_recursively(class_group, class_part_object, class_group_id, theme)
97
+ private def process_classes_recursively(class_group, class_part_object, class_group_id)
101
98
  class_group.each do |class_definition|
102
99
  if class_definition.is_a?(String)
103
100
  class_part_object_to_edit = class_definition.empty? ? class_part_object : get_class_part(class_part_object, class_definition)
104
101
  class_part_object_to_edit[:class_group_id] = class_group_id
105
- next
106
- end
107
-
108
- if class_definition.is_a?(Proc)
102
+ elsif class_definition.is_a?(Proc)
109
103
  if from_theme?(class_definition)
104
+ process_classes_recursively(class_definition.call(@config), class_part_object, class_group_id)
105
+ else
106
+ class_part_object[:validators] << {
107
+ validator: class_definition,
108
+ class_group_id: class_group_id,
109
+ }
110
+ end
111
+ else
112
+ class_definition.each do |key, nested_class_group|
110
113
  process_classes_recursively(
111
- class_definition.call(@config),
112
- class_part_object,
114
+ nested_class_group,
115
+ get_class_part(class_part_object, key),
113
116
  class_group_id,
114
- theme,
115
117
  )
116
- next
117
118
  end
118
-
119
- class_part_object[:validators].push({
120
- validator: class_definition,
121
- class_group_id: class_group_id,
122
- })
123
-
124
- next
125
- end
126
-
127
- class_definition.each do |(key, class_group)|
128
- process_classes_recursively(
129
- class_group,
130
- get_class_part(class_part_object, key),
131
- class_group_id,
132
- theme,
133
- )
134
119
  end
135
120
  end
136
121
  end
@@ -153,16 +138,13 @@ module TailwindMerge
153
138
  end
154
139
 
155
140
  private def get_group_id_for_arbitrary_property(class_name)
156
- if ARBITRARY_PROPERTY_REGEX.match?(class_name)
157
- match = ARBITRARY_PROPERTY_REGEX.match(class_name) || ""
158
- arbitrary_property_class_name = match[1] || ""
159
- property = arbitrary_property_class_name[0...arbitrary_property_class_name.index(":")]
160
-
161
- if !property.nil? && !property.empty?
162
- # uses two dots here because one dot is used as prefix for class groups in plugins
163
- "arbitrary..#{property}"
164
- end
165
- end
141
+ match = ARBITRARY_PROPERTY_REGEX.match(class_name)
142
+ return unless match
143
+
144
+ property = match[1].to_s.split(":", 2).first
145
+
146
+ # Use two dots here because one dot is used as prefix for class groups in plugins
147
+ "arbitrary..#{property}" if property && !property.empty?
166
148
  end
167
149
 
168
150
  private def from_theme?(validator)
@@ -1,66 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module TailwindMerge
4
6
  module Config
5
7
  include Validators
6
8
 
7
- FROM_THEME = ->(config, key) {
8
- config[:theme].fetch(key, nil)
9
- }
10
-
11
- COLORS = ->(config) { FROM_THEME.call(config, "colors") }
12
- SPACING = ->(config) { FROM_THEME.call(config, "spacing") }
13
- BLUR = ->(config) { FROM_THEME.call(config, "blur") }
14
- BRIGHTNESS = ->(config) { FROM_THEME.call(config, "brightness") }
15
- BORDER_COLOR = ->(config) { FROM_THEME.call(config, "border-color") }
16
- BORDER_RADIUS = ->(config) { FROM_THEME.call(config, "border-radius") }
17
- BORDER_SPACING = ->(config) { FROM_THEME.call(config, "border-spacing") }
18
- BORDER_WIDTH = ->(config) { FROM_THEME.call(config, "border-width") }
19
- CONTRAST = ->(config) { FROM_THEME.call(config, "contrast") }
20
- GRAYSCALE = ->(config) { FROM_THEME.call(config, "grayscale") }
21
- HUE_ROTATE = ->(config) { FROM_THEME.call(config, "hue-rotate") }
22
- INVERT = ->(config) { FROM_THEME.call(config, "invert") }
23
- GAP = ->(config) { FROM_THEME.call(config, "gap") }
24
- GRADIENT_COLOR_STOPS = ->(config) { FROM_THEME.call(config, "gradient-color-stops") }
25
- GRADIENT_COLOR_STOP_POSITIONS = ->(config) { FROM_THEME.call(config, "gradient-color-stop-positions") }
26
- INSET = ->(config) { FROM_THEME.call(config, "inset") }
27
- MARGIN = ->(config) { FROM_THEME.call(config, "margin") }
28
- OPACITY = ->(config) { FROM_THEME.call(config, "opacity") }
29
- PADDING = ->(config) { FROM_THEME.call(config, "padding") }
30
- SATURATE = ->(config) { FROM_THEME.call(config, "saturate") }
31
- SCALE = ->(config) { FROM_THEME.call(config, "scale") }
32
- SEPIA = ->(config) { FROM_THEME.call(config, "sepia") }
33
- SKEW = ->(config) { FROM_THEME.call(config, "skew") }
34
- SPACE = ->(config) { FROM_THEME.call(config, "space") }
35
- TRANSLATE = ->(config) { FROM_THEME.call(config, "translate") }
9
+ THEME_KEYS = [
10
+ "colors",
11
+ "spacing",
12
+ "blur",
13
+ "brightness",
14
+ "border-color",
15
+ "border-radius",
16
+ "border-spacing",
17
+ "border-width",
18
+ "contrast",
19
+ "grayscale",
20
+ "hue-rotate",
21
+ "invert",
22
+ "gap",
23
+ "gradient-color-stops",
24
+ "gradient-color-stop-positions",
25
+ "inset",
26
+ "margin",
27
+ "opacity",
28
+ "padding",
29
+ "saturate",
30
+ "scale",
31
+ "sepia",
32
+ "skew",
33
+ "space",
34
+ "translate",
35
+ ].freeze
36
+ THEME_KEYS.each do |key|
37
+ const_set(key.upcase.tr("-", "_"), ->(config) { config[:theme].fetch(key, nil) })
38
+ end
36
39
 
37
- VALID_THEME_IDS = Set.new([
38
- COLORS.object_id,
39
- SPACING.object_id,
40
- BLUR.object_id,
41
- BRIGHTNESS.object_id,
42
- BORDER_COLOR.object_id,
43
- BORDER_RADIUS.object_id,
44
- BORDER_SPACING.object_id,
45
- BORDER_WIDTH.object_id,
46
- CONTRAST.object_id,
47
- GRAYSCALE.object_id,
48
- HUE_ROTATE.object_id,
49
- INVERT.object_id,
50
- GAP.object_id,
51
- GRADIENT_COLOR_STOPS.object_id,
52
- GRADIENT_COLOR_STOP_POSITIONS.object_id,
53
- INSET.object_id,
54
- MARGIN.object_id,
55
- OPACITY.object_id,
56
- PADDING.object_id,
57
- SATURATE.object_id,
58
- SCALE.object_id,
59
- SEPIA.object_id,
60
- SKEW.object_id,
61
- SPACE.object_id,
62
- TRANSLATE.object_id,
63
- ]).freeze
40
+ VALID_THEME_IDS = Set.new(THEME_KEYS.map { |theme_key| const_get(theme_key.upcase.tr("-", "_")).object_id }).freeze
64
41
 
65
42
  OVERSCROLL = -> { ["auto", "contain", "none"] }
66
43
  OVERFLOW = -> { ["auto", "hidden", "clip", "visible", "scroll"] }
@@ -109,6 +86,7 @@ module TailwindMerge
109
86
 
110
87
  DEFAULTS = {
111
88
  cache_size: 500,
89
+ ignore_empty_cache: true,
112
90
  separator: ":",
113
91
  theme: {
114
92
  "colors" => [IS_ANY],
@@ -4,21 +4,20 @@ module TailwindMerge
4
4
  module ModifierUtils
5
5
  IMPORTANT_MODIFIER = "!"
6
6
 
7
- def split_modifiers(class_name, separator: nil)
8
- separator ||= ":"
7
+ def split_modifiers(class_name, separator: ":")
9
8
  separator_length = separator.length
10
- seperator_is_single_char = separator_length == 1
11
- first_seperator_char = separator[0]
9
+ separator_is_single_char = (separator_length == 1)
10
+ first_separator_char = separator[0]
12
11
 
13
12
  modifiers = []
14
13
  bracket_depth = 0
15
14
  modifier_start = 0
16
- postfix_modifier_position = 0
15
+ postfix_modifier_position = nil
17
16
 
18
17
  class_name.each_char.with_index do |char, index|
19
18
  if bracket_depth.zero?
20
- if char == first_seperator_char && (seperator_is_single_char || class_name[index..(index + separator_length - 1)] == separator)
21
- modifiers << class_name[modifier_start..index]
19
+ if char == first_separator_char && (separator_is_single_char || class_name[index, separator_length] == separator)
20
+ modifiers << class_name[modifier_start...index]
22
21
  modifier_start = index + separator_length
23
22
  next
24
23
  elsif char == "/"
@@ -27,17 +26,17 @@ module TailwindMerge
27
26
  end
28
27
  end
29
28
 
30
- if char == "["
31
- bracket_depth += 1
32
- elsif char == "]"
33
- bracket_depth -= 1
34
- end
29
+ bracket_depth += 1 if char == "["
30
+ bracket_depth -= 1 if char == "]"
35
31
  end
36
32
 
37
- base_class_name_with_important_modifier = modifiers.empty? ? class_name : class_name[modifier_start..-1]
33
+ base_class_name_with_important_modifier = modifiers.empty? ? class_name : class_name[modifier_start..]
38
34
  has_important_modifier = base_class_name_with_important_modifier.start_with?(IMPORTANT_MODIFIER)
39
- base_class_name = has_important_modifier ? base_class_name_with_important_modifier[1..-1] : base_class_name_with_important_modifier
40
- maybe_postfix_modifier_position = postfix_modifier_position && postfix_modifier_position > modifier_start ? postfix_modifier_position - modifier_start : false
35
+ base_class_name = has_important_modifier ? base_class_name_with_important_modifier[1..] : base_class_name_with_important_modifier
36
+
37
+ maybe_postfix_modifier_position = if postfix_modifier_position && postfix_modifier_position > modifier_start
38
+ postfix_modifier_position - modifier_start
39
+ end
41
40
 
42
41
  [modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position]
43
42
  end
@@ -46,25 +45,22 @@ module TailwindMerge
46
45
  # - Predefined modifiers are sorted alphabetically
47
46
  # - When an arbitrary variant appears, it must be preserved which modifiers are before and after it
48
47
  def sort_modifiers(modifiers)
49
- if modifiers.length <= 1
50
- return modifiers
51
- end
48
+ return modifiers if modifiers.size <= 1
52
49
 
53
50
  sorted_modifiers = []
54
51
  unsorted_modifiers = []
55
52
 
56
53
  modifiers.each do |modifier|
57
- is_arbitrary_variant = modifier[0] == "["
58
-
59
- if is_arbitrary_variant
60
- sorted_modifiers.push(unsorted_modifiers.sort, modifier)
61
- unsorted_modifiers = []
54
+ if modifier.start_with?("[")
55
+ sorted_modifiers.concat(unsorted_modifiers.sort)
56
+ sorted_modifiers << modifier
57
+ unsorted_modifiers.clear
62
58
  else
63
- unsorted_modifiers.push(modifier)
59
+ unsorted_modifiers << modifier
64
60
  end
65
61
  end
66
62
 
67
- sorted_modifiers.push(...unsorted_modifiers.sort)
63
+ sorted_modifiers.concat(unsorted_modifiers.sort)
68
64
 
69
65
  sorted_modifiers
70
66
  end
@@ -8,12 +8,10 @@ module TailwindMerge
8
8
  def arbitrary_value?(class_part, label, test_value)
9
9
  match = ARBITRARY_VALUE_REGEX.match(class_part)
10
10
  return false unless match
11
+ return test_value.call(match[2]) if match[1].nil?
12
+ return label == match[1] if label.is_a?(String)
11
13
 
12
- unless match[1].nil?
13
- return label.is_a?(Set) ? label.include?(match[1]) : label == match[1]
14
- end
15
-
16
- test_value.call(match[2])
14
+ label.include?(match[1])
17
15
  end
18
16
 
19
17
  def numeric?(x)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TailwindMerge
4
- VERSION = "0.14.0"
4
+ VERSION = "0.16.0"
5
5
  end
@@ -19,23 +19,21 @@ module TailwindMerge
19
19
  SPLIT_CLASSES_REGEX = /\s+/
20
20
 
21
21
  def initialize(config: {})
22
- @config = if config.fetch(:theme, nil)
22
+ @config = if config.key?(:theme)
23
23
  merge_configs(config)
24
24
  else
25
25
  TailwindMerge::Config::DEFAULTS.merge(config)
26
26
  end
27
27
 
28
28
  @class_utils = TailwindMerge::ClassUtils.new(@config)
29
- @cache = LruRedux::Cache.new(@config[:cache_size])
29
+ @cache = LruRedux::Cache.new(@config[:cache_size], @config[:ignore_empty_cache])
30
30
  end
31
31
 
32
32
  def merge(classes)
33
- if classes.is_a?(Array)
34
- classes = classes.compact.join(" ")
35
- end
33
+ normalized = classes.is_a?(Array) ? classes.compact.join(" ") : classes.to_s
36
34
 
37
- @cache.getset(classes) do
38
- merge_class_list(classes).freeze
35
+ @cache.getset(normalized) do
36
+ merge_class_list(normalized).freeze
39
37
  end
40
38
  end
41
39
 
@@ -45,37 +43,38 @@ module TailwindMerge
45
43
  # @example 'float'
46
44
  # @example 'hover:focus:bg-color'
47
45
  # @example 'md:!pr'
48
- class_groups_in_conflict = []
49
- class_names = class_list.strip.split(SPLIT_CLASSES_REGEX)
50
-
51
- result = ""
46
+ trimmed = class_list.strip
47
+ return "" if trimmed.empty?
52
48
 
53
- i = class_names.length - 1
49
+ class_groups_in_conflict = Set.new
54
50
 
55
- loop do
56
- break if i < 0
51
+ merged_classes = []
57
52
 
58
- original_class_name = class_names[i]
53
+ trimmed.split(SPLIT_CLASSES_REGEX).reverse_each do |original_class_name|
54
+ modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position =
55
+ split_modifiers(original_class_name, separator: @config[:separator])
59
56
 
60
- modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position = split_modifiers(original_class_name, separator: @config[:separator])
57
+ actual_base_class_name = if maybe_postfix_modifier_position
58
+ base_class_name[0...maybe_postfix_modifier_position]
59
+ else
60
+ base_class_name
61
+ end
61
62
 
62
- actual_base_class_name = maybe_postfix_modifier_position ? base_class_name[0...maybe_postfix_modifier_position] : base_class_name
63
+ has_postfix_modifier = maybe_postfix_modifier_position ? true : false
63
64
  class_group_id = @class_utils.class_group_id(actual_base_class_name)
64
65
 
65
66
  unless class_group_id
66
- unless maybe_postfix_modifier_position
67
- # not a Tailwind class
68
- result = original_class_name + (!result.empty? ? " " + result : result)
69
- i -= 1
67
+ unless has_postfix_modifier
68
+ # Not a Tailwind class
69
+ merged_classes << original_class_name
70
70
  next
71
71
  end
72
72
 
73
73
  class_group_id = @class_utils.class_group_id(base_class_name)
74
74
 
75
75
  unless class_group_id
76
- # not a Tailwind class
77
- result = original_class_name + (!result.empty? ? " " + result : result)
78
- i -= 1
76
+ # Not a Tailwind class
77
+ merged_classes << original_class_name
79
78
  next
80
79
  end
81
80
 
@@ -87,25 +86,20 @@ module TailwindMerge
87
86
  modifier_id = has_important_modifier ? "#{variant_modifier}#{IMPORTANT_MODIFIER}" : variant_modifier
88
87
  class_id = "#{modifier_id}#{class_group_id}"
89
88
 
90
- # Tailwind class omitted due to pre-existing conflict
91
- if class_groups_in_conflict.include?(class_id)
92
- i -= 1
93
- next
94
- end
89
+ # Tailwind class omitted due to conflict
90
+ next if class_groups_in_conflict.include?(class_id)
95
91
 
96
- class_groups_in_conflict.push(class_id)
92
+ class_groups_in_conflict << class_id
97
93
 
98
- @class_utils.get_conflicting_class_group_ids(class_group_id, has_postfix_modifier).each do |group|
99
- class_groups_in_conflict.push("#{modifier_id}#{group}")
94
+ @class_utils.get_conflicting_class_group_ids(class_group_id, has_postfix_modifier).each do |conflicting_id|
95
+ class_groups_in_conflict << "#{modifier_id}#{conflicting_id}"
100
96
  end
101
97
 
102
- # no conflict!
103
- result = original_class_name + (!result.empty? ? " " + result : result)
104
-
105
- i -= 1
98
+ # Tailwind class not in conflict
99
+ merged_classes << original_class_name
106
100
  end
107
101
 
108
- result
102
+ merged_classes.reverse.join(" ")
109
103
  end
110
104
  end
111
105
  end
@@ -5,21 +5,24 @@ require_relative "lib/tailwind_merge/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "tailwind_merge"
7
7
  spec.version = TailwindMerge::VERSION
8
+ spec.summary = "Utility function to efficiently merge Tailwind CSS classes without style conflicts."
8
9
  spec.authors = ["Garen J. Torikian"]
9
10
  spec.email = ["gjtorikian@gmail.com"]
10
-
11
- spec.summary = "Utility function to efficiently merge Tailwind CSS classes without style conflicts."
12
- spec.homepage = "https://www.github.com/gjtorikian/tailwind_merge"
13
11
  spec.license = "MIT"
14
12
 
15
- spec.required_ruby_version = [">= 3.1", "< 4.0"]
16
-
13
+ github_root_uri = "https://github.com/gjtorikian/tailwind_merge"
14
+ spec.homepage = "#{github_root_uri}/tree/v#{spec.version}"
17
15
  spec.metadata = {
18
- "funding_uri" => "https://github.com/sponsors/gjtorikian/",
16
+ "homepage_uri" => spec.homepage,
17
+ "source_code_uri" => spec.homepage,
18
+ "changelog_uri" => "#{github_root_uri}/blob/v#{spec.version}/CHANGELOG.md",
19
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
20
+ "documentation_uri" => "https://rubydoc.info/gems/#{spec.name}/#{spec.version}",
21
+ "funding_uri" => "https://github.com/sponsors/gjtorikian",
19
22
  "rubygems_mfa_required" => "true",
20
23
  }
21
24
 
22
- spec.metadata["homepage_uri"] = spec.homepage
25
+ spec.required_ruby_version = [">= 3.1", "< 4.0"]
23
26
 
24
27
  # Specify which files should be added to the gem when it is released.
25
28
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -32,7 +35,7 @@ Gem::Specification.new do |spec|
32
35
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
36
  spec.require_paths = ["lib"]
34
37
 
35
- spec.add_dependency("lru_redux", "~> 1.1")
38
+ spec.add_dependency("sin_lru_redux", "~> 2.5")
36
39
 
37
40
  spec.add_development_dependency("minitest", "~> 5.6")
38
41
  spec.add_development_dependency("minitest-focus", "~> 1.1")
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tailwind_merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Garen J. Torikian
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-19 00:00:00.000000000 Z
11
+ date: 2025-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: lru_redux
14
+ name: sin_lru_redux
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.1'
19
+ version: '2.5'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.1'
26
+ version: '2.5'
27
27
  force_ruby_platform: false
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: minitest
@@ -74,13 +74,17 @@ files:
74
74
  - lib/tailwind_merge/validators.rb
75
75
  - lib/tailwind_merge/version.rb
76
76
  - tailwind_merge.gemspec
77
- homepage: https://www.github.com/gjtorikian/tailwind_merge
77
+ homepage: https://github.com/gjtorikian/tailwind_merge/tree/v0.16.0
78
78
  licenses:
79
79
  - MIT
80
80
  metadata:
81
- funding_uri: https://github.com/sponsors/gjtorikian/
81
+ homepage_uri: https://github.com/gjtorikian/tailwind_merge/tree/v0.16.0
82
+ source_code_uri: https://github.com/gjtorikian/tailwind_merge/tree/v0.16.0
83
+ changelog_uri: https://github.com/gjtorikian/tailwind_merge/blob/v0.16.0/CHANGELOG.md
84
+ bug_tracker_uri: https://github.com/gjtorikian/tailwind_merge/issues
85
+ documentation_uri: https://rubydoc.info/gems/tailwind_merge/0.16.0
86
+ funding_uri: https://github.com/sponsors/gjtorikian
82
87
  rubygems_mfa_required: 'true'
83
- homepage_uri: https://www.github.com/gjtorikian/tailwind_merge
84
88
  post_install_message:
85
89
  rdoc_options: []
86
90
  require_paths: