tailwind_merge 0.14.0 → 0.16.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: 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: