Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and improve implementation #50

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 30 additions & 36 deletions lib/tailwind_merge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Merger
SPLIT_CLASSES_REGEX = /\s+/

def initialize(config: {})
@config = if config.fetch(:theme, nil)
@config = if config.key?(:theme)
merge_configs(config)
else
TailwindMerge::Config::DEFAULTS.merge(config)
Expand All @@ -30,12 +30,10 @@ def initialize(config: {})
end

def merge(classes)
if classes.is_a?(Array)
classes = classes.compact.join(" ")
end
normalized = classes.is_a?(Array) ? classes.compact.join(" ") : classes.to_s

@cache.getset(classes) do
merge_class_list(classes).freeze
@cache.getset(normalized) do
merge_class_list(normalized).freeze
end
end

Expand All @@ -45,37 +43,38 @@ def merge(classes)
# @example 'float'
# @example 'hover:focus:bg-color'
# @example 'md:!pr'
class_groups_in_conflict = []
class_names = class_list.strip.split(SPLIT_CLASSES_REGEX)

result = ""
trimmed = class_list.strip
return "" if trimmed.empty?

i = class_names.length - 1
class_groups_in_conflict = Set.new

loop do
break if i < 0
merged_classes = []

original_class_name = class_names[i]
trimmed.split(SPLIT_CLASSES_REGEX).reverse_each do |original_class_name|
modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position =
split_modifiers(original_class_name, separator: @config[:separator])

modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position = split_modifiers(original_class_name, separator: @config[:separator])
actual_base_class_name = if maybe_postfix_modifier_position
base_class_name[0...maybe_postfix_modifier_position]
else
base_class_name
end

actual_base_class_name = maybe_postfix_modifier_position ? base_class_name[0...maybe_postfix_modifier_position] : base_class_name
has_postfix_modifier = maybe_postfix_modifier_position ? true : false
class_group_id = @class_utils.class_group_id(actual_base_class_name)

unless class_group_id
unless maybe_postfix_modifier_position
# not a Tailwind class
result = original_class_name + (!result.empty? ? " " + result : result)
i -= 1
unless has_postfix_modifier
# Not a Tailwind class
merged_classes << original_class_name
next
end

class_group_id = @class_utils.class_group_id(base_class_name)

unless class_group_id
# not a Tailwind class
result = original_class_name + (!result.empty? ? " " + result : result)
i -= 1
# Not a Tailwind class
merged_classes << original_class_name
next
end

Expand All @@ -87,25 +86,20 @@ def merge(classes)
modifier_id = has_important_modifier ? "#{variant_modifier}#{IMPORTANT_MODIFIER}" : variant_modifier
class_id = "#{modifier_id}#{class_group_id}"

# Tailwind class omitted due to pre-existing conflict
if class_groups_in_conflict.include?(class_id)
i -= 1
next
end
# Tailwind class omitted due to conflict
next if class_groups_in_conflict.include?(class_id)

class_groups_in_conflict.push(class_id)
class_groups_in_conflict << class_id

@class_utils.get_conflicting_class_group_ids(class_group_id, has_postfix_modifier).each do |group|
class_groups_in_conflict.push("#{modifier_id}#{group}")
@class_utils.get_conflicting_class_group_ids(class_group_id, has_postfix_modifier).each do |conflicting_id|
class_groups_in_conflict << "#{modifier_id}#{conflicting_id}"
end

# no conflict!
result = original_class_name + (!result.empty? ? " " + result : result)

i -= 1
# Tailwind class not in conflict
merged_classes << original_class_name
end

result
merged_classes.reverse.join(" ")
end
end
end
96 changes: 39 additions & 57 deletions lib/tailwind_merge/class_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(config)
def class_group_id(class_name)
class_parts = class_name.split(CLASS_PART_SEPARATOR)

# Classes like `-inset-1` produce an empty string as first classPart.
# Classes like `-inset-1` produce an empty string as first class_part.
# Assume that classes for negative values are used correctly and remove it from class_parts.
class_parts.shift if class_parts.first == "" && class_parts.length != 1

Expand All @@ -30,52 +30,47 @@ def get_group_recursive(class_parts, class_part_object)

next_class_part_object = class_part_object[:next_part][current_class_part]

class_group_from_next_class_part = next_class_part_object ? get_group_recursive(class_parts[1..-1], next_class_part_object) : nil

return class_group_from_next_class_part if class_group_from_next_class_part
if next_class_part_object
class_group_from_next_class_part = get_group_recursive(class_parts.drop(1), next_class_part_object)
return class_group_from_next_class_part if class_group_from_next_class_part
end

return if class_part_object[:validators].empty?

class_rest = class_parts.join(CLASS_PART_SEPARATOR)

result = class_part_object[:validators].find do |v|
validator = v[:validator]

if from_theme?(validator)
validator.call(@config)
else
validator.call(class_rest)
end
from_theme?(validator) ? validator.call(@config) : validator.call(class_rest)
end

result.nil? ? result : result[:class_group_id]
result&.fetch(:class_group_id, nil)
end

def get_conflicting_class_group_ids(class_group_id, has_postfix_modifier)
conflicts = @config[:conflicting_class_groups][class_group_id] || []

if has_postfix_modifier && @config[:conflicting_class_group_modifiers][class_group_id]
return [...conflicts, ...@config[:conflicting_class_group_modifiers][class_group_id]]
return [*conflicts, *@config[:conflicting_class_group_modifiers][class_group_id]]
end

conflicts
end

private def create_class_map(config)
theme = config[:theme]
prefix = config[:prefix]
class_map = {
next_part: {},
validators: [],
}

prefixed_class_group_entries = get_prefixed_class_group_entries(
config[:class_groups].map { |cg| [cg[0], cg[1]] },
config[:class_groups].map { |group_id, group_classes| [group_id, group_classes] },
prefix,
)

prefixed_class_group_entries.each do |(class_group_id, class_group)|
process_classes_recursively(class_group, class_map, class_group_id, theme)
prefixed_class_group_entries.each do |class_group_id, class_group|
process_classes_recursively(class_group, class_map, class_group_id)
end

class_map
Expand All @@ -84,53 +79,43 @@ def get_conflicting_class_group_ids(class_group_id, has_postfix_modifier)
private def get_prefixed_class_group_entries(class_group_entries, prefix)
return class_group_entries if prefix.nil?

class_group_entries.map do |(class_group_id, class_group)|
class_group_entries.map do |class_group_id, class_group|
prefixed_class_group = class_group.map do |class_definition|
next("#{prefix}#{class_definition}") if class_definition.is_a?(String)

next(class_definition.transform_keys { |key| "#{prefix}#{key}" }) if class_definition.is_a?(Hash)

class_definition
if class_definition.is_a?(String)
"#{prefix}#{class_definition}"
elsif class_definition.is_a?(Hash)
class_definition.transform_keys { |key| "#{prefix}#{key}" }
else
class_definition
end
end

[class_group_id, prefixed_class_group]
end
end

private def process_classes_recursively(class_group, class_part_object, class_group_id, theme)
private def process_classes_recursively(class_group, class_part_object, class_group_id)
class_group.each do |class_definition|
if class_definition.is_a?(String)
class_part_object_to_edit = class_definition.empty? ? class_part_object : get_class_part(class_part_object, class_definition)
class_part_object_to_edit[:class_group_id] = class_group_id
next
end

if class_definition.is_a?(Proc)
elsif class_definition.is_a?(Proc)
if from_theme?(class_definition)
process_classes_recursively(class_definition.call(@config), class_part_object, class_group_id)
else
class_part_object[:validators] << {
validator: class_definition,
class_group_id: class_group_id,
}
end
else
class_definition.each do |key, nested_class_group|
process_classes_recursively(
class_definition.call(@config),
class_part_object,
nested_class_group,
get_class_part(class_part_object, key),
class_group_id,
theme,
)
next
end

class_part_object[:validators].push({
validator: class_definition,
class_group_id: class_group_id,
})

next
end

class_definition.each do |(key, class_group)|
process_classes_recursively(
class_group,
get_class_part(class_part_object, key),
class_group_id,
theme,
)
end
end
end
Expand All @@ -153,16 +138,13 @@ def get_conflicting_class_group_ids(class_group_id, has_postfix_modifier)
end

private def get_group_id_for_arbitrary_property(class_name)
if ARBITRARY_PROPERTY_REGEX.match?(class_name)
match = ARBITRARY_PROPERTY_REGEX.match(class_name) || ""
arbitrary_property_class_name = match[1] || ""
property = arbitrary_property_class_name[0...arbitrary_property_class_name.index(":")]

if !property.nil? && !property.empty?
# uses two dots here because one dot is used as prefix for class groups in plugins
"arbitrary..#{property}"
end
end
match = ARBITRARY_PROPERTY_REGEX.match(class_name)
return unless match

property = match[1].to_s.split(":", 2).first

# Use two dots here because one dot is used as prefix for class groups in plugins
"arbitrary..#{property}" if property && !property.empty?
end

private def from_theme?(validator)
Expand Down
4 changes: 3 additions & 1 deletion lib/tailwind_merge/config.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "set"

module TailwindMerge
module Config
include Validators
Expand Down Expand Up @@ -30,7 +32,7 @@ module Config
"skew",
"space",
"translate",
]
].freeze
THEME_KEYS.each do |key|
const_set(key.upcase.tr("-", "_"), ->(config) { config[:theme].fetch(key, nil) })
end
Expand Down
46 changes: 21 additions & 25 deletions lib/tailwind_merge/modifier_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ module TailwindMerge
module ModifierUtils
IMPORTANT_MODIFIER = "!"

def split_modifiers(class_name, separator: nil)
separator ||= ":"
def split_modifiers(class_name, separator: ":")
separator_length = separator.length
seperator_is_single_char = separator_length == 1
first_seperator_char = separator[0]
separator_is_single_char = (separator_length == 1)
first_separator_char = separator[0]

modifiers = []
bracket_depth = 0
modifier_start = 0
postfix_modifier_position = 0
postfix_modifier_position = nil

class_name.each_char.with_index do |char, index|
if bracket_depth.zero?
if char == first_seperator_char && (seperator_is_single_char || class_name[index..(index + separator_length - 1)] == separator)
modifiers << class_name[modifier_start..index]
if char == first_separator_char && (separator_is_single_char || class_name[index, separator_length] == separator)
modifiers << class_name[modifier_start...index]
modifier_start = index + separator_length
next
elsif char == "/"
Expand All @@ -27,17 +26,17 @@ def split_modifiers(class_name, separator: nil)
end
end

if char == "["
bracket_depth += 1
elsif char == "]"
bracket_depth -= 1
end
bracket_depth += 1 if char == "["
bracket_depth -= 1 if char == "]"
end

base_class_name_with_important_modifier = modifiers.empty? ? class_name : class_name[modifier_start..-1]
base_class_name_with_important_modifier = modifiers.empty? ? class_name : class_name[modifier_start..]
has_important_modifier = base_class_name_with_important_modifier.start_with?(IMPORTANT_MODIFIER)
base_class_name = has_important_modifier ? base_class_name_with_important_modifier[1..-1] : base_class_name_with_important_modifier
maybe_postfix_modifier_position = postfix_modifier_position && postfix_modifier_position > modifier_start ? postfix_modifier_position - modifier_start : false
base_class_name = has_important_modifier ? base_class_name_with_important_modifier[1..] : base_class_name_with_important_modifier

maybe_postfix_modifier_position = if postfix_modifier_position && postfix_modifier_position > modifier_start
postfix_modifier_position - modifier_start
end

[modifiers, has_important_modifier, base_class_name, maybe_postfix_modifier_position]
end
Expand All @@ -46,25 +45,22 @@ def split_modifiers(class_name, separator: nil)
# - Predefined modifiers are sorted alphabetically
# - When an arbitrary variant appears, it must be preserved which modifiers are before and after it
def sort_modifiers(modifiers)
if modifiers.length <= 1
return modifiers
end
return modifiers if modifiers.size <= 1

sorted_modifiers = []
unsorted_modifiers = []

modifiers.each do |modifier|
is_arbitrary_variant = modifier[0] == "["

if is_arbitrary_variant
sorted_modifiers.push(unsorted_modifiers.sort, modifier)
unsorted_modifiers = []
if modifier.start_with?("[")
sorted_modifiers.concat(unsorted_modifiers.sort)
sorted_modifiers << modifier
unsorted_modifiers.clear
else
unsorted_modifiers.push(modifier)
unsorted_modifiers << modifier
end
end

sorted_modifiers.push(...unsorted_modifiers.sort)
sorted_modifiers.concat(unsorted_modifiers.sort)

sorted_modifiers
end
Expand Down
Loading
Loading