# frozen_string_literal: true require_relative "classify/utilities" require_relative "classify/validation" module Ariadne # :nodoc: class Classify FLEX_VALUES = [1, :auto].freeze FLEX_WRAP_MAPPINGS = { wrap: "flex-wrap", nowrap: "flex-nowrap", reverse: "flex-wrap-reverse", }.freeze FLEX_ALIGN_SELF_VALUES = [:auto, :start, :end, :center, :baseline, :stretch].freeze FLEX_DIRECTION_VALUES = [:column, :column_reverse, :row, :row_reverse].freeze FLEX_JUSTIFY_CONTENT_VALUES = [:flex_start, :flex_end, :center, :space_between, :space_around].freeze FLEX_ALIGN_ITEMS_VALUES = [:flex_start, :flex_end, :center, :baseline, :stretch].freeze LOOKUP = Ariadne::Classify::Utilities::UTILITIES class << self # Utility for mapping component configuration into Tailwind CSS class names. # # args can contain utility keys that mimic the interface used by # Ariadne components, as well as the special entries :classes # and :style. # # Returns a hash containing two entries. The :classes entry is a string of # Tailwind CSS class names, including any classes given in the :classes entry # in args. The :style entry is the value of the given :style entry given in # args. # # # Example usage: # extract_css_attrs({ mt: 4, py: 2 }) => { classes: "mt-4 py-2", style: nil } # extract_css_attrs(classes: "d-flex", mt: 4, py: 2) => { classes: "d-flex mt-4 py-2", style: nil } # extract_css_attrs(classes: "d-flex", style: "float: left", mt: 4, py: 2) => { classes: "d-flex mt-4 py-2", style: "float: left" } # def call(args) style = nil args = [] if args.blank? classes = [].tap do |result| args.each do |key, val| case key when :classes # insert :classes first to avoid huge doc diffs if (class_names = validated_class_names(val)) result.unshift(class_names) end next when :style style = val next end next unless LOOKUP[key] if val.is_a?(Array) # A while loop is ~3.5x faster than Array#each. brk = 0 while brk < val.size item = val[brk] if item.nil? brk += 1 next end # Believe it or not, three calls to Hash#[] and an inline rescue # are about 30% faster than Hash#dig. It also ensures validate is # only called when necessary, i.e. when the class can't be found # in the lookup table. # rubocop:disable Style/RescueModifier found = (LOOKUP[key][item][brk] rescue nil) || validate(key, item, brk) # rubocop:enable Style/RescueModifier result << found if found brk += 1 end else next if val.nil? # rubocop:disable Style/RescueModifier found = (LOOKUP[key][val][0] rescue nil) || validate(key, val, 0) # rubocop:enable Style/RescueModifier result << found if found end end end.join(" ") result = {} result[:class] = classes if classes.present? result[:style] = style if style.present? result end private def validate(key, val, brk) brk_str = Ariadne::Classify::Utilities::BREAKPOINTS[brk] Ariadne::Classify::Utilities.validate(key, val, brk_str) end private def validated_class_names(classes) return if classes.blank? corrected_classes = correct_classes(classes) if raise_on_invalid_options? && !ENV["ARIADNE_WARNINGS_DISABLED"] invalid_class_names = corrected_classes.each_with_object([]) do |class_name, memo| memo << class_name if Ariadne::Classify::Validation.invalid?(class_name) end # TODO: implement this if invalid_class_names.any? # raise ArgumentError, <<~MSG # Use Tailwind CSS class names instead of your own #{"name".pluralize(invalid_class_names.length)} #{invalid_class_names.to_sentence}. # Set ARIADNE_WARNINGS_DISABLED=1 to disable this warning. # MSG end end corrected_classes.join(" ") end # TODO: automate this, ugh. peek at utilities.yml BG_PREFIX = /^bg-/.freeze BG_PSEUDO_PREFIX = /^\S+:bg-/.freeze BORDER_PREFIX = /^border-/.freeze BORDER_PSEUDO_PREFIX = /^\S+:border-/.freeze TEXT_ASPECT_PREFIX = /^text-\S+-/.freeze TEXT_ASPECT_PSEUDO_PREFIX = /^\S+:text-\S+-/.freeze TEXT_PREFIX = /^text-/.freeze TEXT_PSEUDO_PREFIX = /^\S+:text-/.freeze # TODO: TEST! private def correct_classes(classes) matched_bg = "" matched_bg_pseudo = {} matched_border = "" matched_border_pseudo = {} matched_text_aspect = {} matched_text_aspect_pseudo = {} matched_text = "" matched_text_pseudo = {} classes.split(" ").reverse.each_with_object([]) do |c, memo| next if c.blank? class_name = c.strip if class_name.match(BG_PREFIX) next if matched_bg.present? memo << matched_bg = class_name elsif class_name.match(BG_PSEUDO_PREFIX) next if matched_bg_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) } matched_bg_pseudo[class_name] = true memo << class_name elsif class_name.match(BORDER_PREFIX) next if matched_border.present? memo << matched_border = class_name elsif class_name.match(BORDER_PSEUDO_PREFIX) next if matched_border_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) } matched_border_pseudo[class_name] = true memo << class_name elsif class_name.match(TEXT_ASPECT_PREFIX) next if matched_text_aspect.keys.any? { |m| m.start_with?(class_name.split(":").first) } matched_text_aspect[class_name] = true memo << class_name elsif class_name.match(TEXT_ASPECT_PSEUDO_PREFIX) next if matched_text_aspect_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) } matched_text_aspect_pseudo[class_name] = true memo << class_name elsif class_name.match(TEXT_PREFIX) next if matched_text.present? memo << matched_text = class_name elsif class_name.match(TEXT_PSEUDO_PREFIX) next if matched_text_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) } matched_text_pseudo[class_name] = true memo << class_name else memo << class_name end end.uniq end private def raise_on_invalid_options? Rails.application.config.ariadne_view_components.raise_on_invalid_options end end end end