lib/tailwind_merge/validators.rb



# frozen_string_literal: true

require "set"

module TailwindMerge
  module Validators
    class << self
      def arbitrary_value?(class_part, label, test_value)
        match = ARBITRARY_VALUE_REGEX.match(class_part)
        return false unless match

        unless match[1].nil?
          return label.is_a?(Set) ? label.include?(match[1]) : label == match[1]
        end

        test_value.call(match[2])
      end

      def numeric?(x)
        Float(x, exception: false).is_a?(Numeric)
      end

      def integer?(x)
        Integer(x, exception: false).is_a?(Integer)
      end
    end

    STRING_LENGTHS = Set.new(["px", "full", "screen"]).freeze

    ARBITRARY_VALUE_REGEX = /^\[(?:([a-z-]+):)?(.+)\]$/i
    FRACTION_REGEX = %r{^\d+/\d+$}
    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$/
    TSHIRT_UNIT_REGEX = /^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/
    COLOR_FUNCTION_REGEX = /^(rgba?|hsla?|hwb|(ok)?(lab|lch))\(.+\)$/
    # Shadow always begins with x and y offset separated by underscore optionally prepended by inset
    SHADOW_REGEX = /^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/
    IMAGE_REGEX = /^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/

    SIZE_LABELS = Set.new(["length", "size", "percentage"]).freeze
    IMAGE_LABELS = Set.new(["image", "url"]).freeze

    is_length_only = ->(value) {
      # `colorFunctionRegex` check is necessary because color functions can have percentages in them which which would be incorrectly classified as lengths.
      # For example, `hsl(0 0% 0%)` would be classified as a length without this check.
      # I could also use lookbehind assertion in `lengthUnitRegex` but that isn't supported widely enough.
      LENGTH_UNIT_REGEX.match?(value) && !COLOR_FUNCTION_REGEX.match?(value)
    }

    is_never = ->(_) { false }

    is_number = ->(value) {
      numeric?(value)
    }

    is_integer_only = ->(value) {
      integer?(value)
    }

    is_shadow = ->(value) {
      SHADOW_REGEX.match?(value)
    }

    is_image = ->(value) {
      IMAGE_REGEX.match?(value)
    }

    IS_LENGTH = ->(value) {
      numeric?(value) ||
        STRING_LENGTHS.include?(value) ||
        FRACTION_REGEX.match?(value)
    }

    IS_ARBITRARY_LENGTH = ->(value) {
      arbitrary_value?(value, "length", is_length_only)
    }

    IS_ARBITRARY_NUMBER = ->(value) {
      arbitrary_value?(value, "number", is_number)
    }

    IS_NUMBER = ->(value) {
      is_number.call(value)
    }

    IS_INTEGER = ->(value) {
      is_integer_only.call(value)
    }

    IS_PERCENT = ->(value) {
      value.end_with?("%") && is_number.call(value[0..-2])
    }

    IS_ARBITRARY_VALUE = ->(value) {
      ARBITRARY_VALUE_REGEX.match(value)
    }

    IS_TSHIRT_SIZE = ->(value) {
      TSHIRT_UNIT_REGEX.match?(value)
    }

    IS_ARBITRARY_SIZE = ->(value) {
      arbitrary_value?(value, SIZE_LABELS, is_never)
    }

    IS_ARBITRARY_POSITION = ->(value) {
      arbitrary_value?(value, "position", is_never)
    }

    IS_ARBITRARY_IMAGE = ->(value) {
      arbitrary_value?(value, IMAGE_LABELS, is_image)
    }

    IS_ARBITRARY_SHADOW = ->(value) {
      arbitrary_value?(value, "", is_shadow)
    }

    IS_ANY = ->(_) { true }
  end
end