tailwind_merge 0.16.0 → 1.0.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.
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TailwindMerge
4
+ class TailwindClass < Struct.new(:is_external, :modifiers, :has_important_modifier, :base_class_name, :maybe_postfix_modifier_position)
5
+ end
6
+
7
+ module ParseClassName
8
+ IMPORTANT_MODIFIER = "!"
9
+ MODIFIER_SEPARATOR = ":"
10
+ MODIFIER_SEPARATOR_LENGTH = MODIFIER_SEPARATOR.length
11
+
12
+ ##
13
+ # Parse class name into parts.
14
+ #
15
+ # Inspired by `splitAtTopLevelOnly` used in Tailwind CSS
16
+ # @see https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js
17
+ def parse_class_name(class_name, prefix: nil)
18
+ unless prefix.nil?
19
+ full_prefix = "#{prefix}#{MODIFIER_SEPARATOR}"
20
+ if class_name.start_with?(full_prefix)
21
+ return parse_class_name(class_name[full_prefix.length..])
22
+ else
23
+ return TailwindClass.new(
24
+ is_external: true,
25
+ modifiers: [],
26
+ has_important_modifier: false,
27
+ base_class_name: class_name,
28
+ maybe_postfix_modifier_position: nil,
29
+ )
30
+ end
31
+ end
32
+
33
+ modifiers = []
34
+
35
+ bracket_depth = 0
36
+ paren_depth = 0
37
+ modifier_start = 0
38
+ postfix_modifier_position = nil
39
+
40
+ class_name.each_char.with_index do |char, index|
41
+ if bracket_depth.zero? && paren_depth.zero?
42
+ if char == MODIFIER_SEPARATOR
43
+ modifiers << class_name[modifier_start...index]
44
+ modifier_start = index + MODIFIER_SEPARATOR_LENGTH
45
+ next
46
+ elsif char == "/"
47
+ postfix_modifier_position = index
48
+ next
49
+ end
50
+ end
51
+
52
+ bracket_depth += 1 if char == "["
53
+ bracket_depth -= 1 if char == "]"
54
+ paren_depth += 1 if char == "("
55
+ paren_depth -= 1 if char == ")"
56
+ end
57
+
58
+ base_class_name_with_important_modifier = modifiers.empty? ? class_name : class_name[modifier_start..]
59
+
60
+ base_class_name, has_important_modifier = strip_important_modifier(base_class_name_with_important_modifier)
61
+
62
+ maybe_postfix_modifier_position = if postfix_modifier_position && postfix_modifier_position > modifier_start
63
+ postfix_modifier_position - modifier_start
64
+ end
65
+
66
+ TailwindClass.new(
67
+ is_external: false,
68
+ modifiers:,
69
+ has_important_modifier:,
70
+ base_class_name: base_class_name,
71
+ maybe_postfix_modifier_position:,
72
+ )
73
+ end
74
+
75
+ def strip_important_modifier(base_class_name)
76
+ if base_class_name.end_with?(IMPORTANT_MODIFIER)
77
+ return [base_class_name[0...-IMPORTANT_MODIFIER.length], true]
78
+ end
79
+
80
+ # In Tailwind CSS v3 the important modifier was at the start of the base class name. This is still supported for legacy reasons.
81
+ # @see https://github.com/dcastil/tailwind-merge/issues/513#issuecomment-2614029864
82
+ if base_class_name.start_with?(IMPORTANT_MODIFIER)
83
+ return [base_class_name[1..], true]
84
+ end
85
+
86
+ [base_class_name, false]
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TailwindMerge
4
+ module SortModifiers
5
+ # Sorts modifiers according to following schema:
6
+ # - Predefined modifiers are sorted alphabetically
7
+ # - When an arbitrary variant appears, it must be preserved which modifiers are before and after it
8
+ def sort_modifiers(modifiers, order_sensitive_modifiers)
9
+ return modifiers if modifiers.size <= 1
10
+
11
+ sorted_modifiers = []
12
+ unsorted_modifiers = []
13
+
14
+ modifiers.each do |modifier|
15
+ is_position_sensitive = modifier.start_with?("[") || order_sensitive_modifiers.include?(modifier)
16
+
17
+ if is_position_sensitive
18
+ sorted_modifiers.concat(unsorted_modifiers.sort)
19
+ sorted_modifiers << modifier
20
+ unsorted_modifiers.clear
21
+ else
22
+ unsorted_modifiers << modifier
23
+ end
24
+ end
25
+
26
+ sorted_modifiers.concat(unsorted_modifiers.sort)
27
+
28
+ sorted_modifiers
29
+ end
30
+ end
31
+ end
@@ -5,13 +5,22 @@ require "set"
5
5
  module TailwindMerge
6
6
  module Validators
7
7
  class << self
8
- def arbitrary_value?(class_part, label, test_value)
8
+ def arbitrary_value?(class_part, test_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)
13
11
 
14
- label.include?(match[1])
12
+ return test_label.call(match[1]) unless match[1].nil?
13
+
14
+ test_value.call(match[2])
15
+ end
16
+
17
+ def arbitrary_variable?(class_part, test_label, should_match_no_label: false)
18
+ match = ARBITRARY_VARIABLE_REGEX.match(class_part)
19
+ return false unless match
20
+
21
+ return test_label.call(match[1]) unless match[1].nil?
22
+
23
+ should_match_no_label
15
24
  end
16
25
 
17
26
  def numeric?(x)
@@ -23,95 +32,150 @@ module TailwindMerge
23
32
  end
24
33
  end
25
34
 
26
- STRING_LENGTHS = Set.new(["px", "full", "screen"]).freeze
27
-
28
- ARBITRARY_VALUE_REGEX = /^\[(?:([a-z-]+):)?(.+)\]$/i
35
+ ARBITRARY_VALUE_REGEX = /^\[(?:(\w[\w-]*):)?(.+)\]$/i
36
+ ARBITRARY_VARIABLE_REGEX = /^\((?:(\w[\w-]*):)?(.+)\)$/i
29
37
  FRACTION_REGEX = %r{^\d+/\d+$}
30
- LENGTH_UNIT_REGEX = /\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/
31
38
  TSHIRT_UNIT_REGEX = /^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/
39
+ LENGTH_UNIT_REGEX = /\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/
32
40
  COLOR_FUNCTION_REGEX = /^(rgba?|hsla?|hwb|(ok)?(lab|lch))\(.+\)$/
41
+
33
42
  # Shadow always begins with x and y offset separated by underscore optionally prepended by inset
34
43
  SHADOW_REGEX = /^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/
35
44
  IMAGE_REGEX = /^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/
36
45
 
37
- SIZE_LABELS = Set.new(["length", "size", "percentage"]).freeze
38
- IMAGE_LABELS = Set.new(["image", "url"]).freeze
46
+ IS_FRACTION = ->(value) {
47
+ FRACTION_REGEX.match?(value)
48
+ }
49
+
50
+ IS_NUMBER = ->(value) {
51
+ numeric?(value)
52
+ }
53
+
54
+ IS_INTEGER = ->(value) {
55
+ integer?(value)
56
+ }
57
+
58
+ IS_PERCENT = ->(value) {
59
+ value.end_with?("%") && IS_NUMBER.call(value[0..-2])
60
+ }
61
+
62
+ IS_TSHIRT_SIZE = ->(value) {
63
+ TSHIRT_UNIT_REGEX.match?(value)
64
+ }
65
+
66
+ IS_ANY = ->(_ = nil) { true }
39
67
 
40
- is_length_only = ->(value) {
68
+ IS_LENGTH_ONLY = ->(value) {
41
69
  # `colorFunctionRegex` check is necessary because color functions can have percentages in them which which would be incorrectly classified as lengths.
42
70
  # For example, `hsl(0 0% 0%)` would be classified as a length without this check.
43
71
  # I could also use lookbehind assertion in `lengthUnitRegex` but that isn't supported widely enough.
44
72
  LENGTH_UNIT_REGEX.match?(value) && !COLOR_FUNCTION_REGEX.match?(value)
45
73
  }
46
74
 
47
- is_never = ->(_) { false }
75
+ IS_NEVER = ->(_) { false }
48
76
 
49
- is_number = ->(value) {
50
- numeric?(value)
77
+ IS_SHADOW = ->(value) {
78
+ SHADOW_REGEX.match?(value)
51
79
  }
52
80
 
53
- is_integer_only = ->(value) {
54
- integer?(value)
81
+ IS_IMAGE = ->(value) {
82
+ IMAGE_REGEX.match?(value)
55
83
  }
56
84
 
57
- is_shadow = ->(value) {
58
- SHADOW_REGEX.match?(value)
85
+ IS_ANY_NON_ARBITRARY = ->(value) {
86
+ !IS_ARBITRARY_VALUE.call(value) && !IS_ARBITRARY_VARIABLE.call(value)
59
87
  }
60
88
 
61
- is_image = ->(value) {
62
- IMAGE_REGEX.match?(value)
89
+ IS_ARBITRARY_SIZE = ->(value) {
90
+ arbitrary_value?(value, IS_LABEL_SIZE, IS_NEVER)
63
91
  }
64
92
 
65
- IS_LENGTH = ->(value) {
66
- numeric?(value) ||
67
- STRING_LENGTHS.include?(value) ||
68
- FRACTION_REGEX.match?(value)
93
+ IS_ARBITRARY_VALUE = ->(value) {
94
+ ARBITRARY_VALUE_REGEX.match(value)
69
95
  }
70
96
 
71
97
  IS_ARBITRARY_LENGTH = ->(value) {
72
- arbitrary_value?(value, "length", is_length_only)
98
+ arbitrary_value?(value, IS_LABEL_LENGTH, IS_LENGTH_ONLY)
73
99
  }
74
100
 
75
101
  IS_ARBITRARY_NUMBER = ->(value) {
76
- arbitrary_value?(value, "number", is_number)
102
+ arbitrary_value?(value, IS_LABEL_NUMBER, IS_NUMBER)
77
103
  }
78
104
 
79
- IS_NUMBER = ->(value) {
80
- is_number.call(value)
105
+ IS_ARBITRARY_POSITION = ->(value) {
106
+ arbitrary_value?(value, IS_LABEL_POSITION, IS_NEVER)
81
107
  }
82
108
 
83
- IS_INTEGER = ->(value) {
84
- is_integer_only.call(value)
109
+ IS_ARBITRARY_IMAGE = ->(value) {
110
+ arbitrary_value?(value, IS_LABEL_IMAGE, IS_IMAGE)
85
111
  }
86
112
 
87
- IS_PERCENT = ->(value) {
88
- value.end_with?("%") && is_number.call(value[0..-2])
113
+ IS_ARBITRARY_SHADOW = ->(value) {
114
+ arbitrary_value?(value, IS_NEVER, IS_SHADOW)
89
115
  }
90
116
 
91
- IS_ARBITRARY_VALUE = ->(value) {
92
- ARBITRARY_VALUE_REGEX.match(value)
117
+ IS_ARBITRARY_VARIABLE = ->(value) {
118
+ ARBITRARY_VARIABLE_REGEX.match(value)
93
119
  }
94
120
 
95
- IS_TSHIRT_SIZE = ->(value) {
96
- TSHIRT_UNIT_REGEX.match?(value)
121
+ IS_ARBITRARY_VARIABLE_LENGTH = ->(value) {
122
+ arbitrary_variable?(value, IS_LABEL_LENGTH)
97
123
  }
98
124
 
99
- IS_ARBITRARY_SIZE = ->(value) {
100
- arbitrary_value?(value, SIZE_LABELS, is_never)
125
+ IS_ARBITRARY_VARIABLE_FAMILY_NAME = ->(value) {
126
+ arbitrary_variable?(value, IS_LABEL_FAMILY_NAME)
101
127
  }
102
128
 
103
- IS_ARBITRARY_POSITION = ->(value) {
104
- arbitrary_value?(value, "position", is_never)
129
+ IS_ARBITRARY_VARIABLE_POSITION = ->(value) {
130
+ arbitrary_variable?(value, IS_LABEL_POSITION)
105
131
  }
106
132
 
107
- IS_ARBITRARY_IMAGE = ->(value) {
108
- arbitrary_value?(value, IMAGE_LABELS, is_image)
133
+ IS_ARBITRARY_VARIABLE_SIZE = ->(value) {
134
+ arbitrary_variable?(value, IS_LABEL_SIZE)
109
135
  }
110
136
 
111
- IS_ARBITRARY_SHADOW = ->(value) {
112
- arbitrary_value?(value, "", is_shadow)
137
+ IS_ARBITRARY_VARIABLE_IMAGE = ->(value) {
138
+ arbitrary_variable?(value, IS_LABEL_IMAGE)
139
+ }
140
+
141
+ IS_ARBITRARY_VARIABLE_SHADOW = ->(value) {
142
+ arbitrary_variable?(value, IS_LABEL_SHADOW, should_match_no_label: true)
113
143
  }
114
144
 
115
- IS_ANY = ->(_) { true }
145
+ ############
146
+ # Labels
147
+ ############
148
+
149
+ IS_LABEL_POSITION = ->(value) {
150
+ value == "position"
151
+ }
152
+
153
+ IMAGE_LABELS = Set.new(["image", "url"]).freeze
154
+
155
+ IS_LABEL_IMAGE = ->(value) {
156
+ IMAGE_LABELS.include?(value)
157
+ }
158
+
159
+ SIZE_LABELS = Set.new(["length", "size", "percentage"]).freeze
160
+
161
+ IS_LABEL_SIZE = ->(value) {
162
+ SIZE_LABELS.include?(value)
163
+ }
164
+
165
+ IS_LABEL_LENGTH = ->(value) {
166
+ value == "length"
167
+ }
168
+
169
+ IS_LABEL_NUMBER = ->(value) {
170
+ value == "number"
171
+ }
172
+
173
+ IS_LABEL_FAMILY_NAME = ->(value) {
174
+ value == "family-name"
175
+ }
176
+
177
+ IS_LABEL_SHADOW = ->(value) {
178
+ value == "shadow"
179
+ }
116
180
  end
117
181
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TailwindMerge
4
- VERSION = "0.16.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -5,8 +5,9 @@ require "lru_redux"
5
5
  require_relative "tailwind_merge/version"
6
6
  require_relative "tailwind_merge/validators"
7
7
  require_relative "tailwind_merge/config"
8
- require_relative "tailwind_merge/class_utils"
9
- require_relative "tailwind_merge/modifier_utils"
8
+ require_relative "tailwind_merge/class_group_utils"
9
+ require_relative "tailwind_merge/sort_modifiers"
10
+ require_relative "tailwind_merge/parse_class_name"
10
11
 
11
12
  require "strscan"
12
13
  require "set"
@@ -14,18 +15,15 @@ require "set"
14
15
  module TailwindMerge
15
16
  class Merger
16
17
  include Config
17
- include ModifierUtils
18
+ include ParseClassName
19
+ include SortModifiers
18
20
 
19
21
  SPLIT_CLASSES_REGEX = /\s+/
20
22
 
21
23
  def initialize(config: {})
22
- @config = if config.key?(:theme)
23
- merge_configs(config)
24
- else
25
- TailwindMerge::Config::DEFAULTS.merge(config)
26
- end
27
-
28
- @class_utils = TailwindMerge::ClassUtils.new(@config)
24
+ @config = merge_config(config)
25
+ @config[:important_modifier] = @config[:important_modifier].to_s
26
+ @class_utils = TailwindMerge::ClassGroupUtils.new(@config)
29
27
  @cache = LruRedux::Cache.new(@config[:cache_size], @config[:ignore_empty_cache])
30
28
  end
31
29
 
@@ -51,16 +49,20 @@ module TailwindMerge
51
49
  merged_classes = []
52
50
 
53
51
  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])
56
-
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
52
+ result = parse_class_name(original_class_name, prefix: @config[:prefix])
53
+ is_external = result.is_external
54
+ modifiers = result.modifiers
55
+ has_important_modifier = result.has_important_modifier
56
+ base_class_name = result.base_class_name
57
+ maybe_postfix_modifier_position = result.maybe_postfix_modifier_position
58
+
59
+ if is_external
60
+ merged_classes.push(original_class_name)
61
+ next
61
62
  end
62
63
 
63
64
  has_postfix_modifier = maybe_postfix_modifier_position ? true : false
65
+ actual_base_class_name = has_postfix_modifier ? base_class_name[0...maybe_postfix_modifier_position] : base_class_name
64
66
  class_group_id = @class_utils.class_group_id(actual_base_class_name)
65
67
 
66
68
  unless class_group_id
@@ -81,7 +83,7 @@ module TailwindMerge
81
83
  has_postfix_modifier = false
82
84
  end
83
85
 
84
- variant_modifier = sort_modifiers(modifiers).join(":")
86
+ variant_modifier = sort_modifiers(modifiers, @config[:order_sensitive_modifiers]).join(":")
85
87
 
86
88
  modifier_id = has_important_modifier ? "#{variant_modifier}#{IMPORTANT_MODIFIER}" : variant_modifier
87
89
  class_id = "#{modifier_id}#{class_group_id}"
data/script/test ADDED
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ #/ Usage: script/test <filename:test_line>
3
+ #/ 1. `script/test FILE` runs all tests in the file.
4
+ #/ 1. `script/test FILE:LINE` runs test in specific line of the file.
5
+ #/ 1. `script/test 'GLOB'` runs all tests for matching glob.
6
+ #/ * make sure to wrap the `GLOB` in single quotes `''`.
7
+
8
+ if ! [ $# -eq 0 ]; then
9
+ export TEST=$(echo "$1" | cut -d ":" -f 1)
10
+
11
+ if [[ "$1" == *":"* ]]; then
12
+ LINE=$(echo "$1" | cut -d ":" -f 2)
13
+ LINE=$(head -n $LINE $TEST | tail -1)
14
+ NAME=$(echo "$LINE" | sed "s/.*def //")
15
+
16
+ if ! [[ "$NAME" == "test_"* ]] && ! [[ "$NAME" == "bench_"* ]]; then
17
+ echo
18
+ echo "ERROR: Line provided does not define a test case"
19
+ exit 1
20
+ fi
21
+
22
+ export TESTOPTS="--name=$NAME"
23
+ fi
24
+ fi
25
+
26
+ # Check for and parse flags
27
+ for arg in "$@"
28
+ do
29
+ case $arg in
30
+ --debug)
31
+ export DEBUG=true
32
+ ;;
33
+ *)
34
+ # Unknown arguments can be ignored or handled here
35
+ ;;
36
+ esac
37
+ done
38
+
39
+ if [[ "$TEST" == "test/system"* ]] ; then
40
+ bundle exec rake test:system
41
+ else
42
+ bundle exec rake test
43
+ fi
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tailwind_merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 1.0.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: 2025-01-25 00:00:00.000000000 Z
11
+ date: 2025-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sin_lru_redux
@@ -68,21 +68,23 @@ files:
68
68
  - README.md
69
69
  - Rakefile
70
70
  - lib/tailwind_merge.rb
71
- - lib/tailwind_merge/class_utils.rb
71
+ - lib/tailwind_merge/class_group_utils.rb
72
72
  - lib/tailwind_merge/config.rb
73
- - lib/tailwind_merge/modifier_utils.rb
73
+ - lib/tailwind_merge/parse_class_name.rb
74
+ - lib/tailwind_merge/sort_modifiers.rb
74
75
  - lib/tailwind_merge/validators.rb
75
76
  - lib/tailwind_merge/version.rb
77
+ - script/test
76
78
  - tailwind_merge.gemspec
77
- homepage: https://github.com/gjtorikian/tailwind_merge/tree/v0.16.0
79
+ homepage: https://github.com/gjtorikian/tailwind_merge/tree/v1.0.0
78
80
  licenses:
79
81
  - MIT
80
82
  metadata:
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
83
+ homepage_uri: https://github.com/gjtorikian/tailwind_merge/tree/v1.0.0
84
+ source_code_uri: https://github.com/gjtorikian/tailwind_merge/tree/v1.0.0
85
+ changelog_uri: https://github.com/gjtorikian/tailwind_merge/blob/v1.0.0/CHANGELOG.md
84
86
  bug_tracker_uri: https://github.com/gjtorikian/tailwind_merge/issues
85
- documentation_uri: https://rubydoc.info/gems/tailwind_merge/0.16.0
87
+ documentation_uri: https://rubydoc.info/gems/tailwind_merge/1.0.0
86
88
  funding_uri: https://github.com/sponsors/gjtorikian
87
89
  rubygems_mfa_required: 'true'
88
90
  post_install_message:
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TailwindMerge
4
- module ModifierUtils
5
- IMPORTANT_MODIFIER = "!"
6
-
7
- def split_modifiers(class_name, separator: ":")
8
- separator_length = separator.length
9
- separator_is_single_char = (separator_length == 1)
10
- first_separator_char = separator[0]
11
-
12
- modifiers = []
13
- bracket_depth = 0
14
- modifier_start = 0
15
- postfix_modifier_position = nil
16
-
17
- class_name.each_char.with_index do |char, index|
18
- if bracket_depth.zero?
19
- if char == first_separator_char && (separator_is_single_char || class_name[index, separator_length] == separator)
20
- modifiers << class_name[modifier_start...index]
21
- modifier_start = index + separator_length
22
- next
23
- elsif char == "/"
24
- postfix_modifier_position = index
25
- next
26
- end
27
- end
28
-
29
- bracket_depth += 1 if char == "["
30
- bracket_depth -= 1 if char == "]"
31
- end
32
-
33
- base_class_name_with_important_modifier = modifiers.empty? ? class_name : class_name[modifier_start..]
34
- has_important_modifier = base_class_name_with_important_modifier.start_with?(IMPORTANT_MODIFIER)
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
40
-
41
- [modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position]
42
- end
43
-
44
- # Sorts modifiers according to following schema:
45
- # - Predefined modifiers are sorted alphabetically
46
- # - When an arbitrary variant appears, it must be preserved which modifiers are before and after it
47
- def sort_modifiers(modifiers)
48
- return modifiers if modifiers.size <= 1
49
-
50
- sorted_modifiers = []
51
- unsorted_modifiers = []
52
-
53
- modifiers.each do |modifier|
54
- if modifier.start_with?("[")
55
- sorted_modifiers.concat(unsorted_modifiers.sort)
56
- sorted_modifiers << modifier
57
- unsorted_modifiers.clear
58
- else
59
- unsorted_modifiers << modifier
60
- end
61
- end
62
-
63
- sorted_modifiers.concat(unsorted_modifiers.sort)
64
-
65
- sorted_modifiers
66
- end
67
- end
68
- end