lib/tailwind_merge/class_group_utils.rb



# frozen_string_literal: true

module TailwindMerge
  class ClassGroupUtils
    attr_reader :class_map

    CLASS_PART_SEPARATOR = "-"

    ARBITRARY_PROPERTY_REGEX = /^\[(.+)\]$/

    def initialize(config)
      @config = config
      @class_map = create_class_map(config)
    end

    def class_group_id(class_name)
      class_parts = class_name.split(CLASS_PART_SEPARATOR)

      # 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

      get_group_recursive(class_parts, @class_map) || get_group_id_for_arbitrary_property(class_name)
    end

    def get_group_recursive(class_parts, class_part_object)
      return class_part_object[:class_group_id] if class_parts.empty?

      current_class_part = class_parts.first

      next_class_part_object = class_part_object[:next_part][current_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]
        from_theme?(validator) ? validator.call(@config) : validator.call(class_rest)
      end

      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]]
      end

      conflicts
    end

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

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

      class_map
    end

    private def process_classes_recursively(class_group, class_part_object, class_group_id, theme)
      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
        elsif class_definition.is_a?(Proc)
          if from_theme?(class_definition)
            process_classes_recursively(class_definition.call(@config), class_part_object, class_group_id, theme)
          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(
              nested_class_group,
              get_class_part(class_part_object, key),
              class_group_id,
              theme,
            )
          end
        end
      end
    end

    private def get_class_part(class_part_object, path)
      current_class_part_object = class_part_object

      path.to_s.split(CLASS_PART_SEPARATOR).each do |path_part|
        unless current_class_part_object[:next_part].key?(path_part)
          current_class_part_object[:next_part][path_part] = {
            next_part: {},
            validators: [],
          }
        end

        current_class_part_object = current_class_part_object[:next_part][path_part]
      end

      current_class_part_object
    end

    private def get_group_id_for_arbitrary_property(class_name)
      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)
      TailwindMerge::Config::VALID_THEME_IDS.include?(validator.object_id)
    end
  end
end