lib/tailwind_merge/validators.rb
# frozen_string_literal: true require "set" module TailwindMerge module Validators class << self def arbitrary_value?(class_part, test_label, test_value) match = ARBITRARY_VALUE_REGEX.match(class_part) return false unless match return test_label.call(match[1]) unless match[1].nil? test_value.call(match[2]) end def arbitrary_variable?(class_part, test_label, should_match_no_label: false) match = ARBITRARY_VARIABLE_REGEX.match(class_part) return false unless match return test_label.call(match[1]) unless match[1].nil? should_match_no_label end def numeric?(x) Float(x, exception: false).is_a?(Numeric) end def integer?(x) Integer(x, exception: false).is_a?(Integer) end end ARBITRARY_VALUE_REGEX = /^\[(?:(\w[\w-]*):)?(.+)\]$/i ARBITRARY_VARIABLE_REGEX = /^\((?:(\w[\w-]*):)?(.+)\)$/i FRACTION_REGEX = %r{^\d+/\d+$} TSHIRT_UNIT_REGEX = /^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/ 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$/ COLOR_FUNCTION_REGEX = /^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/ # 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)\(.+\)$/ IS_FRACTION = ->(value) { FRACTION_REGEX.match?(value) } IS_NUMBER = ->(value) { numeric?(value) } IS_INTEGER = ->(value) { integer?(value) } IS_PERCENT = ->(value) { value.end_with?("%") && IS_NUMBER.call(value[0..-2]) } IS_TSHIRT_SIZE = ->(value) { TSHIRT_UNIT_REGEX.match?(value) } IS_ANY = ->(_ = nil) { true } 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_SHADOW = ->(value) { SHADOW_REGEX.match?(value) } IS_IMAGE = ->(value) { IMAGE_REGEX.match?(value) } IS_ANY_NON_ARBITRARY = ->(value) { !IS_ARBITRARY_VALUE.call(value) && !IS_ARBITRARY_VARIABLE.call(value) } IS_ARBITRARY_SIZE = ->(value) { arbitrary_value?(value, IS_LABEL_SIZE, IS_NEVER) } IS_ARBITRARY_VALUE = ->(value) { ARBITRARY_VALUE_REGEX.match(value) } IS_ARBITRARY_LENGTH = ->(value) { arbitrary_value?(value, IS_LABEL_LENGTH, IS_LENGTH_ONLY) } IS_ARBITRARY_NUMBER = ->(value) { arbitrary_value?(value, IS_LABEL_NUMBER, IS_NUMBER) } IS_ARBITRARY_POSITION = ->(value) { arbitrary_value?(value, IS_LABEL_POSITION, IS_NEVER) } IS_ARBITRARY_IMAGE = ->(value) { arbitrary_value?(value, IS_LABEL_IMAGE, IS_IMAGE) } IS_ARBITRARY_SHADOW = ->(value) { arbitrary_value?(value, IS_LABEL_SHADOW, IS_SHADOW) } IS_ARBITRARY_VARIABLE = ->(value) { ARBITRARY_VARIABLE_REGEX.match(value) } IS_ARBITRARY_VARIABLE_LENGTH = ->(value) { arbitrary_variable?(value, IS_LABEL_LENGTH) } IS_ARBITRARY_VARIABLE_FAMILY_NAME = ->(value) { arbitrary_variable?(value, IS_LABEL_FAMILY_NAME) } IS_ARBITRARY_VARIABLE_POSITION = ->(value) { arbitrary_variable?(value, IS_LABEL_POSITION) } IS_ARBITRARY_VARIABLE_SIZE = ->(value) { arbitrary_variable?(value, IS_LABEL_SIZE) } IS_ARBITRARY_VARIABLE_IMAGE = ->(value) { arbitrary_variable?(value, IS_LABEL_IMAGE) } IS_ARBITRARY_VARIABLE_SHADOW = ->(value) { arbitrary_variable?(value, IS_LABEL_SHADOW, should_match_no_label: true) } ############ # Labels ############ IS_LABEL_POSITION = ->(label) { label == "position" || label == "percentage" } IS_LABEL_IMAGE = ->(label) { label == "image" || label == "url" } IS_LABEL_SIZE = ->(label) { label == "length" || label == "size" || label == "bg-size" } IS_LABEL_LENGTH = ->(label) { label == "length" } IS_LABEL_NUMBER = ->(label) { label == "number" } IS_LABEL_FAMILY_NAME = ->(label) { label == "family-name" } IS_LABEL_SHADOW = ->(label) { label == "shadow" } end end