tailwind_merge 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 11bdac9bc7e383d03e99dfcde6ed68cbb68a66949471a588d9b20b1fde0fe131
4
+ data.tar.gz: d406c2b1fc35551201b90fc45091141e1361f0205312c32101af44d72179e139
5
+ SHA512:
6
+ metadata.gz: d6f89f756d614e384e769be8f45b64530c051159b9a2eff5a5e0159ea58c20332c14de2e25d9e1304d174846bb094ebc34d24d1b6ff1a0bf0802f27a2f91d08b
7
+ data.tar.gz: 5ff6834884a37392265f131afae80a0fc8f7296a83073ab65afd44bc546ac9b5a6964ec156b1cc3e0e04ca15e135083da72055f4b9fd9cbc54c07ed5094619cd
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_gem:
2
+ rubocop-standard:
3
+ - config/default.yml
4
+ - config/minitest.yml
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-07-17
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in tailwind_merge.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "rubocop", "~> 1.21"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Garen J. Torikian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # TailwindMerge
2
+
3
+ Utility function to efficiently merge [Tailwind CSS](https://tailwindcss.com/) classes without style conflicts. Essentially, a Ruby port of [tailwind-merge](https://github.com/dcastil/tailwind-merge).
4
+
5
+ Supports Tailwind v3.0+.
6
+
7
+ ## Installation
8
+
9
+ Install the gem and add it to your application's Gemfile by executing:
10
+
11
+ $ bundle add tailwind_merge
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install tailwind_merge
16
+
17
+ ```ruby
18
+ require "tailwind_merge"
19
+
20
+ TailwindMerge::Merger.new.merge("px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]")
21
+ # → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
22
+ ```
23
+
24
+ ## What's it for?
25
+
26
+ If you use Tailwind with a component-based UI renderer (like [ViewComponent](https://viewcomponent.org) or [Ariadne](https://github.com/yettoapp/ariadne/tree/main/ruby/ariadne_view_components)), you're probably familiar with the situation that you want to change some styles of an existing component:
27
+
28
+ ```html
29
+ <!-- app/components/confirm_email_component.html.erb -->
30
+ <div class="border rounded px-2 py-1">
31
+ Please confirm your email address.
32
+ </div>
33
+ ```
34
+
35
+ ```ruby
36
+ <%= render(ConfirmEmailComponent.new(class: "p-5")) %>
37
+ ```
38
+
39
+ When the `ConfirmEmailComponent` is rendered, an input with the className `border rounded px-2 py-1` gets created. But because of the way the [CSS cascade](https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade) works, the styles of the `p-5` class are ignored. The order of the classes in the `class` string doesn't matter at all and the only way to apply the `p-3` styles is to remove both `px-2` and `py-1`.
40
+
41
+ This is where `tailwind_merge` comes in:
42
+
43
+ ```ruby
44
+
45
+ @merger = TailwindMerge::Merger.new
46
+ @merger.merge("border rounded px-2 py-1 px-5")
47
+ # border rounded p-5
48
+ ```
49
+
50
+ tailwind-merge overrides conflicting classes and keeps everything else untouched. In the case of the your implementation of `ConfirmEmailComponent`, the input now only renders the classes `border rounded p-5`.
51
+
52
+ ## Features
53
+
54
+ ### Optimized for speed
55
+
56
+ - Results get cached by default, so you don't need to worry about wasteful re-renders. The library uses a thread-safe [LRU cache](<https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)>) which stores up to 500 different results. The cache size can be modified via config options.
57
+ - Expensive computations happen upfront so that `merge` calls without a cache hit stay fast.
58
+ - These computations are called lazily on the first call to `merge` to prevent it from impacting app startup performance if it isn't used initially.
59
+
60
+ ### Last conflicting class wins
61
+
62
+ ```ruby
63
+ @merger.merge('p-5 p-2 p-4') # → 'p-4'
64
+ ```
65
+
66
+ ### Supports refinements
67
+
68
+ ```ruby
69
+ @merger.merge('p-3 px-5') # → 'p-3 px-5'
70
+ @merger.merge('inset-x-4 right-4') # → 'inset-x-4 right-4'
71
+ ```
72
+
73
+ ### Resolves non-trivial conflicts
74
+
75
+ ```ruby
76
+ twMe@merger.mergerge('inset-x-px -inset-1') # → '-inset-1'
77
+ @merger.merge('bottom-auto inset-y-6') # → 'inset-y-6'
78
+ @merger.merge('inline block') # → 'block'
79
+ ```
80
+
81
+ ### Supports modifiers and stacked modifiers
82
+
83
+ ```ruby
84
+ @merger.merge('p-2 hover:p-4') # → 'p-2 hover:p-4'
85
+ @merger.merge('hover:p-2 hover:p-4') # → 'hover:p-4'
86
+ @merger.merge('hover:focus:p-2 focus:hover:p-4') # → 'focus:hover:p-4'
87
+ ```
88
+
89
+ ### Supports arbitrary values
90
+
91
+ ```ruby
92
+ @merger.merge('bg-black bg-[color:var(--mystery-var)]') # → 'bg-[color:var(--mystery-var)]'
93
+ @merger.merge('grid-cols-[1fr,auto] grid-cols-2') # → 'grid-cols-2'
94
+ ```
95
+
96
+ ### Supports arbitrary properties
97
+
98
+ ```ruby
99
+ @merger.merge('[mask-type:luminance] [mask-type:alpha]') # → '[mask-type:alpha]'
100
+ @merger.merge('[--scroll-offset:56px] lg:[--scroll-offset:44px]') # → '[--scroll-offset:56px] lg:[--scroll-offset:44px]'
101
+
102
+ #Don't actually do this!
103
+ @merger.merge('[padding:1rem] p-8') # → '[padding:1rem] p-8'
104
+ ```
105
+
106
+ Watch out when mixing arbitrary properties which could be expressed as Tailwind classes. `tailwind_merge` does not resolve conflicts between arbitrary properties and their matching Tailwind classes.
107
+
108
+ ### Supports arbitrary variants
109
+
110
+ ```ruby
111
+ @merger.merge('[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4') # → '[&:nth-child(3)]:py-4'
112
+ @merger.merge('dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4') # → 'hover:dark:[&:nth-child(3)]:py-4'
113
+
114
+ # Don't actually do this!
115
+ @merger.merge('[&:focus]:ring focus:ring-4') # → '[&:focus]:ring focus:ring-4'
116
+ ```
117
+
118
+ Similarly to arbitrary properties, `tailwind_merge` does not resolve conflicts between arbitrary variants and their matching predefined modifiers.
119
+
120
+ ### Supports important modifier
121
+
122
+ ```ruby
123
+ @merger.merge('!p-3 !p-4 p-5') # → '!p-4 p-5'
124
+ @merger.merge('!right-2 !-inset-x-1') # → '!-inset-x-1'
125
+ ```
126
+
127
+ ### Preserves non-Tailwind classes
128
+
129
+ ```ruby
130
+ @merger.merge('p-5 p-2 my-non-tailwind-class p-4') # → 'my-non-tailwind-class p-4'
131
+ ```
132
+
133
+ ### Supports custom colors out of the box
134
+
135
+ ```ruby
136
+ @merger.merge('text-red text-secret-sauce') # → 'text-secret-sauce'
137
+ ```
138
+
139
+ ## Basic usage
140
+
141
+ If you're using Tailwind CSS without any extra configs, you can use it right away:
142
+
143
+ ```ruby
144
+ merger = TailwindMerge::Merger.new
145
+ ```
146
+
147
+ ## Usage with custom Tailwind config
148
+
149
+ If you're using a custom Tailwind config, you may need to configure tailwind-merge as well to merge classes properly.
150
+
151
+ The default [`twMerge`](#twmerge) function is configured in a way that you can still use it if all the following points apply to your Tailwind config:
152
+
153
+ - Only using color names which don't clash with other Tailwind class names
154
+ - Only deviating by number values from number-based Tailwind classes
155
+ - Only using font-family classes which don't clash with default font-weight classes
156
+ - Sticking to default Tailwind config for everything else
157
+
158
+ If some of these points don't apply to you, you can test whether the merge still works as intended with your custom classes. Otherwise, you need create your own custom merge function by either extending the default tailwind-merge config or using a completely custom one.
159
+
160
+ The `tailwind_merge` config is different from the Tailwind config because it's expected to be shipped and run in the browser as opposed to the Tailwind config which is meant to run at build-time. Be careful in case you're using your Tailwind config directly to configure tailwind-merge in your client-side code because that could result in an unnecessarily large bundle size.
161
+
162
+ ## Configuration
163
+
164
+ The `tailwind_merge` config is an object with several keys:
165
+
166
+ ```ruby
167
+ tailwindMergeConfig = {
168
+ # ↓ Set how many values should be stored in cache.
169
+ cache_size: 500,
170
+ # ↓ Optional prefix from Tailwind config
171
+ prefix: 'tw-',
172
+ theme: {
173
+ # Theme scales are defined here
174
+ # This is not the theme object from your Tailwind config
175
+ },
176
+ class_groups: {
177
+ # Class groups are defined here
178
+ },
179
+ conflicting_class_groups: {
180
+ # Conflicts between class groups are defined here
181
+ },
182
+ }
183
+ ```
184
+
185
+ ### Class groups
186
+
187
+ 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:
188
+
189
+ ```ruby
190
+ position_class_group = ['static', 'fixed', 'absolute', 'relative', 'sticky']
191
+ ```
192
+
193
+ `tailwind_merge` resolves conflicts between classes in a class group and only keeps the last one passed to the merge function call:
194
+
195
+ ```ruby
196
+ @merger.merge('static sticky relative') # → 'relative'
197
+ ```
198
+
199
+ 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.
200
+
201
+ For example, here is the overflow class group which results in the classes `overflow-auto`, `overflow-hidden`, `overflow-visible` and `overflow-scroll`.
202
+
203
+ ```ruby
204
+ overflow_class_group = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]
205
+ ```
206
+
207
+ 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.
208
+
209
+ For example, here is the fill class group:
210
+
211
+ ```ruby
212
+ is_arbitrary_value = (class_part: string) => /^\[.+\]$/.test(class_part)
213
+ fill_class_group = [{ fill: ['current', IS_ARBITRARY_VALUE] }]
214
+ ```
215
+
216
+ 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.
217
+
218
+ 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.
219
+
220
+ ```ruby
221
+ # ↓ Resolves to filter and filter-none
222
+ filter_class_group = [{ filter: ['', 'none'] }]
223
+ ```
224
+
225
+ 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.
226
+
227
+ ### Conflicting class groups
228
+
229
+ Sometimes there are conflicts across Tailwind classes which are more complex than "remove all those other classes when a class from this group is present in the class list string".
230
+
231
+ One example is the combination of the classes `px-3` (setting `padding-left` and `padding-right`) and `pr-4` (setting `padding-right`).
232
+
233
+ If they are passed to `merge` as `pr-4 px-3`, I think you most likely intend to apply `padding-left` and `padding-right` from the `px-3` class and want `pr-4` to be removed, indicating that both these classes should belong to a single class group.
234
+
235
+ But if they are passed to `merge` as `px-3 pr-4`, it's assumed you want to set the `padding-right` from `pr-4` but still want to apply the `padding-left` from `px-3`, so `px-3` shouldn't be removed when inserting the classes in this order, indicating they shouldn't be in the same class group.
236
+
237
+ To summarize, `px-3` should stand in conflict with `pr-4`, but `pr-4` should not stand in conflict with `px-3`. To achieve this, we need to define asymmetric conflicts across class groups.
238
+
239
+ 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.
240
+
241
+ ```ruby
242
+ conflicting_class_groups = {
243
+ px: ['pr', 'pl'],
244
+ }
245
+ ```
246
+
247
+ 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.
248
+
249
+ When we think of our example, the `px` class group creates a conflict which is received by the class groups `pr` and `pl`. This way `px-3` removes a preceding `pr-4`, but not the other way around.
250
+
251
+ ### Theme
252
+
253
+ In the Tailwind config you can modify theme scales. `tailwind_merge` follows the same keys for the theme scales, but doesn't support all of them. It only supports theme scales which are used in multiple class groups. At the moment these are:
254
+
255
+ - `colors`
256
+ - `spacing`
257
+ - `blur`
258
+ - `brightness`
259
+ - `borderColor`
260
+ - `borderRadius`
261
+ - `borderSpacing`
262
+ - `borderWidth`
263
+ - `contrast`
264
+ - `grayscale`
265
+ - `hueRotate`
266
+ - `invert`
267
+ - `gap`
268
+ - `gradientColorStops`
269
+ - `inset`
270
+ - `margin`
271
+ - `opacity`
272
+ - `padding`
273
+ - `saturate`
274
+ - `scale`
275
+ - `sepia`
276
+ - `skew`
277
+ - `space`
278
+ - `translate`
279
+
280
+ If you modified one of these theme scales in your Tailwind config, you can add all your keys right here and tailwind-merge will take care of the rest. If you modified other theme scales, you need to figure out the class group to modify in the [default config](#getdefaultconfig).
281
+
282
+ ### Validators
283
+
284
+ Here's a brief summary for each validator:
285
+
286
+ - `IS_LENGTH` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a arbitrary length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`.
287
+ - `IS_ARBITRARY_LENGTH` checks for arbitrary length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`).
288
+ - `IS_INTEGER` checks for integer values (`3`) and arbitrary integer values (`[3]`).
289
+ - `IS_ARBITRARY_VALUE` checks whether the class part is enclosed in brackets (`[something]`)
290
+ - `IS_TSHIRT_SIZE`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`).
291
+ - `IS_ARBITRARY_SIZE` checks whether class part is an arbitrary value which starts with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames.
292
+ - `IS_ARBITRARY_POSITION` checks whether class part is an arbitrary value which starts with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames.
293
+ - `IS_ARBITRARY_URL` checks whether class part is an arbitrary value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames.
294
+ - `IS_ARBITRARY_WEIGHT` checks whether class part is an arbitrary value which starts with `number:` or is a number (`[number:var(--value)]`, `[450]`) which is necessary for font-weight classNames.
295
+ - `IS_ARBITRARY_SHADOW` checks whether class part is an arbitrary value which starts with the same pattern as a shadow value (`[0_35px_60px_-15px_rgba(0,0,0,0.3)]`), namely with two lengths separated by a underscore.
296
+ - `IS_ANY` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when it's certain there are no other class groups in a namespace.
297
+
298
+ ## Contributing
299
+
300
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gjtorikian/tailwind_merge.
301
+
302
+ ## License
303
+
304
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: [:test, :rubocop]
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TailwindMerge
4
+ class ClassUtils
5
+ attr_reader :class_map
6
+
7
+ CLASS_PART_SEPARATOR = "-"
8
+
9
+ ARBITRARY_PROPERTY_REGEX = /^\[(.+)\]$/
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @class_map = create_class_map(config)
14
+ end
15
+
16
+ def class_group_id(class_name)
17
+ class_parts = class_name.split(CLASS_PART_SEPARATOR)
18
+
19
+ # Classes like `-inset-1` produce an empty string as first classPart.
20
+ # Assume that classes for negative values are used correctly and remove it from class_parts.
21
+ class_parts.shift if class_parts.first == "" && class_parts.length != 1
22
+
23
+ get_group_recursive(class_parts, @class_map) || get_group_id_for_arbitrary_property(class_name)
24
+ end
25
+
26
+ def get_group_recursive(class_parts, class_part_object)
27
+ return class_part_object[:class_group_id] if class_parts.empty?
28
+
29
+ current_class_part = class_parts.first
30
+
31
+ next_class_part_object = class_part_object[:next_part][current_class_part]
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
36
+
37
+ return nil if class_part_object[:validators].empty?
38
+
39
+ class_rest = class_parts.join(CLASS_PART_SEPARATOR)
40
+
41
+ result = class_part_object[:validators].find do |v|
42
+ if from_theme?(v[:validator])
43
+ v[:validator].call(@config)
44
+ else
45
+ v[:validator].call(class_rest)
46
+ end
47
+ end
48
+
49
+ result.nil? ? result : result[:class_group_id]
50
+ end
51
+
52
+ def get_conflicting_class_group_ids(class_group_id)
53
+ @config[:conflicting_class_groups][class_group_id] || []
54
+ end
55
+
56
+ private def create_class_map(config)
57
+ theme = config[:theme]
58
+ prefix = config[:prefix]
59
+ class_map = {
60
+ next_part: {},
61
+ validators: [],
62
+ }
63
+
64
+ prefixed_class_group_entries = get_prefixed_class_group_entries(
65
+ config[:class_groups].map { |cg| [cg[0], cg[1]] },
66
+ prefix
67
+ )
68
+
69
+ prefixed_class_group_entries.each do |(class_group_id, class_group)|
70
+ process_classes_recursively(class_group, class_map, class_group_id, theme)
71
+ end
72
+
73
+ class_map
74
+ end
75
+
76
+ private def get_prefixed_class_group_entries(class_group_entries, prefix)
77
+ return class_group_entries if prefix.nil?
78
+
79
+ class_group_entries.map do |(class_group_id, class_group)|
80
+ prefixed_class_group = class_group.map do |class_definition|
81
+ next("#{prefix}#{class_definition}") if class_definition.is_a?(String)
82
+
83
+ next(class_definition.transform_keys { |key| "#{prefix}#{key}" }) if class_definition.is_a?(Hash)
84
+
85
+ class_definition
86
+ end
87
+
88
+ [class_group_id, prefixed_class_group]
89
+ end
90
+ end
91
+
92
+ private def process_classes_recursively(class_group, class_part_object, class_group_id, theme)
93
+ class_group.each do |class_definition|
94
+ if class_definition.is_a?(String)
95
+ class_part_object_to_edit = class_definition.empty? ? class_part_object : get_class_part(class_part_object, class_definition)
96
+ class_part_object_to_edit[:class_group_id] = class_group_id
97
+ next
98
+ end
99
+
100
+ if class_definition.is_a?(Proc)
101
+ if from_theme?(class_definition)
102
+ process_classes_recursively(
103
+ class_definition.call(@config),
104
+ class_part_object,
105
+ class_group_id,
106
+ theme
107
+ )
108
+ next
109
+ end
110
+
111
+ class_part_object[:validators].push({
112
+ validator: class_definition,
113
+ class_group_id: class_group_id,
114
+ })
115
+
116
+ next
117
+ end
118
+
119
+ class_definition.each do |(key, class_group)|
120
+ process_classes_recursively(
121
+ class_group,
122
+ get_class_part(class_part_object, key),
123
+ class_group_id,
124
+ theme
125
+ )
126
+ end
127
+ end
128
+ end
129
+
130
+ private def get_class_part(class_part_object, path)
131
+ current_class_part_object = class_part_object
132
+
133
+ path.to_s.split(CLASS_PART_SEPARATOR).each do |path_part|
134
+ unless current_class_part_object[:next_part].key?(path_part)
135
+ current_class_part_object[:next_part][path_part] = {
136
+ next_part: {},
137
+ validators: [],
138
+ }
139
+ end
140
+
141
+ current_class_part_object = current_class_part_object[:next_part][path_part]
142
+ end
143
+
144
+ current_class_part_object
145
+ end
146
+
147
+ private def get_group_id_for_arbitrary_property(class_name)
148
+ if ARBITRARY_PROPERTY_REGEX.match?(class_name)
149
+ match = ARBITRARY_PROPERTY_REGEX.match(class_name) || ""
150
+ arbitrary_property_class_name = match[1] || ""
151
+ property = arbitrary_property_class_name[0...arbitrary_property_class_name.index(":")]
152
+
153
+ if !property.nil? && !property.empty?
154
+ # uses two dots here because one dot is used as prefix for class groups in plugins
155
+ "arbitrary..#{property}"
156
+ end
157
+ end
158
+ end
159
+
160
+ private def from_theme?(validator)
161
+ TailwindMerge::Config::VALID_THEME_IDS.include?(validator.object_id)
162
+ end
163
+ end
164
+ end