lib/custom.ex

defmodule Tails.Custom do
  @moduledoc """
  Use to create a custom tails module, that can be configured with `config :your_app, YourTails`

  Additionally, themes can be passed at runtime.

  Example Usage:

  ```elixir
  @themes %{
    default: %{...},
    dark: %{...}
  }
  use Tails.Custom, otp_app: :my_app, themes: @themes
  ```
  """

  defmacro __using__(opts) do
    quote location: :keep,
          generated: true,
          bind_quoted: [
            otp_app: opts[:otp_app],
            themes: opts[:themes],
            dark_themes: opts[:dark_themes]
          ] do
      require Tails.Custom
      otp_app = otp_app || :tails

      if otp_app == :tails do
        @colors_file Application.compile_env(otp_app, :colors_file)
        @color_classes Application.compile_env(otp_app, :color_classes) || []
        @no_merge_classes Application.compile_env(otp_app, :no_merge_classes) || []
        @dark_themes dark_themes || Application.compile_env(otp_app, :dark_themes)
        @themes themes || Application.compile_env(otp_app, :themes)
        @custom_variants Application.compile_env(otp_app, :variants) || []
        @fallback_to_colors Application.compile_env(otp_app, :fallback_to_colors) || false
      else
        @colors_file Application.compile_env(otp_app, __MODULE__)[:colors_file]
        @color_classes Application.compile_env(otp_app, __MODULE__)[:color_classes] || []
        @no_merge_classes Application.compile_env(otp_app, __MODULE__)[:no_merge_classes] || []
        @dark_themes dark_themes || Application.compile_env(otp_app, __MODULE__)[:dark_themes]
        @themes themes || Application.compile_env(otp_app, __MODULE__)[:themes]
        @custom_variants Application.compile_env(otp_app, __MODULE__)[:variants] || []
        @fallback_to_colors Application.compile_env(otp_app, __MODULE__)[:fallback_to_colors] ||
                              false
      end

      @colors (if @colors_file do
                 @external_resource @colors_file

                 custom =
                   @colors_file
                   |> Path.expand()
                   |> File.read!()
                   |> Jason.decode!()

                 Map.merge(Tails.Colors.builtin_colors(), custom)
               else
                 Tails.Colors.builtin_colors()
               end)

      @all_colors Tails.Colors.all_color_classes(@colors) ++ @color_classes

      @colors_by_size @all_colors |> Enum.group_by(&byte_size/1)

      @variants ~w(
        hover focus focus-within focus-visible active visited target first last only odd
        even first-of-type last-of-type only-of-type empty disabled enabled checked
        indeterminate default required valid invalid in-range out-of-range placeholder-shown
        autofill read-only open before after first-letter first-line marker selection file
        backdrop placeholder sm md lg xl 2xl dark portrait landscape motion-safe motion-reduce
        contrast-more contrast-less rtl ltr
      ) ++ @custom_variants

      @font_weights ~w(thin extralight light normal medium semibold bold extrabold black)
      @font_smoothings ~w(antialiased subpixel-antialiased)
      @font_styles ~w(italic non-italic)
      @positions ~w(static fixed absolute relative sticky)
      @display ~w(
        block inline-block inline flex inline-flex table inline-table table-caption table-cell
        table-column table-column-group table-footer-group table-header-group table-row-group
        table-row flow-root grid inline-grid contents list-item hidden
      )
      @bg_sizes ~w(auto cover contain)
      @bg_repeats ~w(repeat no-repeat repeat-x repeat-y repeat-round repeat-space)
      @bg_positions ~w(bottom center left left-bottom left-top right right-bottom right-top top)
      @bg_blends ~w(normal multiply screen overlay darken lighten color-dodge color-burn hard-light soft-light difference exclusion hue saturation color luminosity)
      @bg_origins ~w(border padding content)
      @bg_attachments ~w(fixed local scroll)
      @bg_clips ~w(border padding content text)
      @bg_images ~w(none gradient-to-t gradient-to-tr gradient-to-r gradient-to-br gradient-to-b gradient-to-bl gradient-to-l gradient-to-tl)
      @outline_styles ~w(none dashed dotted double)
      @aspect_ratios ~w(auto square video)
      @text_overflow ~w(truncate text-ellipsis text-clip)
      @break_values ~w(auto avoid all avoid-page page left right column)
      @break_inside_values ~w(auto avoid avoid-page avoid-column)
      @box_decoration_breaks ~w(clone slice)
      @box_sizes ~w(border content)
      @floats ~w(left right none)
      @clears ~w(left right both none)
      @isolations ~w(isolate isolation-auto)
      @flex_directions ~w(row row-reverse col col-reverse)
      @object_fits ~w(contain cover fill none scale-down)
      @object_positions ~w(bottom center left left-bottom left-top right right-bottom right-top top)
      @overflows ~w(auto hidden clip visible scroll)
      @overscrolls ~w(auto contain none)
      @visibilities ~w(visible invisible collapse)
      @flex_wraps ~w(wrap wrap-reverse nowrap)
      @flex_grow_shrinks ~w(1 auto initial none)
      @flex_specific_grow_shrinks ~w(0)
      @grid_flows ~w(row col dense row-dense col-dense)
      @auto_cols_rows ~w(auto min max fr)
      @justify_contents ~w(start end center between around evenly)
      @justify_items ~w(start end center stretch)
      @justify_selfs ~w(auto start end center stretch)
      @align_contents ~w(center start end between around evenly baseline)
      @align_items ~w(start end center baseline stretch)
      @align_selfs ~w(auto start end center stretch baseline)
      @place_contents ~w(center start end between around evenly baseline stretch)
      @place_items ~w(start end center baseline stretch)
      @place_selfs ~w(auto start end center stretch)
      @font_families ~w(sans serif mono)
      @font_variant_numerics ~w(normal-nums ordinal slashed-zero lining-nums oldstyle-nums proportional-nums tabular-nums diagonal-fractions stacked-fractions)
      @trackings ~w(tighter tight normal wide wider widest)
      @list_style_types ~w(none disc decimal)
      @list_style_positions ~w(inside outside)
      @text_alignments ~w(left center right justify start end)
      @text_decoration_styles ~w(solid double dotted dashed wavy)
      @text_decorations ~w(underline overline line-through no-underline)
      @text_transforms ~w(uppercase lowercase capitalize normal-case)
      @text_overflows ~w(truncate text-ellipses text-clip)
      @vertical_alignments ~w(baseline top middle bottom text-top text-bottom sub super)
      @whitespaces ~w(normal nowrap pre pre-line pre-wrap)
      @word_breaks ~w(normal words all keep)
      @border_styles ~w(solid dashed dotted double hidden none)
      @divide_styles ~w(solid dashed dotted double none)
      @outline_styles ~w(none dashed dotted double)
      @blend_modes ~w(
        normal multiply screen overlay darken lighten color-dodge color-burn
        hard-light soft-light difference exclusion hue saturation color luminosity
      )
      @table_layouts ~w(auto fixed)
      @mix_blend_modes @blend_modes ++ ~w(plus-lighter)
      @bg_blend_modes @blend_modes
      @border_collapse_modes ~w(collapse separate)
      @transition_timing_functions ~w(ease-linear ease-in ease-out ease-in-out)
      @transition_properties ~w(none all colors opacity shadow transform)
      @animations ~w(none spin ping pulse bounce)
      @transforms ~w(gpu none)
      @transform_origins ~w(center top top-right right bottom-right bottom bottom-left left top-left)
      @cursors ~w(
        auto default pointer wait text move help not-allowed none context-menu progress cell crosshair
        vertical-text alias copy no-drop grab grabbing all-scroll col-resize row-resize n-resize e-resize
        s-resize w-resize ne-resize nw-resize se-resize sw-resize ew-resize ns-resize nesw-resize nwse-resize
        zoom-in zoom-out
      )
      @pointer_events ~w(auto none)
      @resizes ~w(none y x)
      @scroll_behaviors ~w(auto smooth)
      @snap_aligns ~w(start end center align-none)
      @snap_stops ~w(normal always)
      @snap_types ~w(none x y both)
      @snap_strictness ~w(mandatory proximity)
      @touch_actions ~w(auto none pan-x pan-left pan-right pan-y pan-up pan-down pinch-zoom manipulation)
      @user_selects ~w(none text all auto)
      @will_change ~w(auto scroll contents transform)
      @digits Enum.map(0..9, &to_string/1)

      @prefixed_with_values [
        will_change: %{prefix: "will-change", values: @will_change},
        user_select: %{prefix: "select", values: @user_selects},
        touch_action: %{prefix: "touch", values: @touch_actions},
        snap_type: %{prefix: "snap", values: @snap_types},
        snap_strictness: %{prefix: "snap", values: @snap_strictness},
        snap_align: %{prefix: "snap", values: @snap_aligns},
        snap_stop: %{prefix: "snap", values: @snap_stops},
        scroll_behaviour: %{prefix: "scroll", values: @scroll_behaviors},
        resize: %{prefix: "resize", values: @resizes, naked?: true},
        pointer_events: %{prefix: "pointer-events", values: @pointer_events},
        cursor: %{prefix: "cursor", values: @cursors},
        transform: %{prefix: "transform", values: @transforms},
        animate: %{prefix: "animate", values: @animations},
        transition_property: %{prefix: "transition", values: @transition_properties, naked?: true},
        table_layout: %{prefix: "table", values: @table_layouts},
        border_collapse_mode: %{
          prefix: "border",
          values: @border_collapse_modes,
          no_arbitrary?: true
        },
        mix_blend_mode: %{prefix: "mix-blend", values: @mix_blend_modes},
        bg_blend_mode: %{prefix: "bg-blend", values: @bg_blend_modes},
        outline_styles: %{prefix: "outline", values: @outline_styles, naked?: true},
        divide_styles: %{prefix: "divide", values: @divide_styles},
        border_style: %{prefix: "border", values: @border_styles, no_arbitrary?: true},
        word_breaks: %{prefix: "break", values: @word_breaks},
        whitespace: %{prefix: "whitespace", values: @whitespaces},
        text_align: %{prefix: "text", values: @text_alignments, no_arbitrary?: true},
        vertical_align: %{prefix: "align", values: @vertical_alignments},
        text_decoration_style: %{prefix: "decoration", values: @text_decoration_styles},
        list_style_type: %{prefix: "list", values: @list_style_types},
        list_style_position: %{prefix: "list", values: @list_style_positions},
        tracking: %{prefix: "tracking", values: @trackings},
        place_content: %{prefix: "place-content", values: @place_contents},
        place_items: %{prefix: "place-items", values: @place_items},
        place_selfs: %{prefix: "place-selfs", values: @place_selfs},
        align_content: %{prefix: "content", values: @align_contents},
        align_items: %{prefix: "items", values: @align_items},
        align_selfs: %{prefix: "selfs", values: @align_selfs},
        auto_cols: %{prefix: "auto-cols", values: @auto_cols_rows},
        auto_rows: %{prefix: "auto-rows", values: @auto_cols_rows},
        grid_flow: %{prefix: "grid-flow", values: @grid_flows},
        justify_contents: %{prefix: "justify", values: @justify_contents},
        justify_items: %{prefix: "justify-items", values: @justify_items},
        justify_selfs: %{prefix: "justify-selfs", values: @justify_selfs},
        flex_grow_shrink: %{prefix: "flex", values: @flex_grow_shrinks},
        flex_direction: %{prefix: "flex", values: @flex_directions},
        shrink: %{prefix: "shrink", values: @flex_specific_grow_shrinks, naked?: true},
        grow: %{prefix: "grow", values: @flex_specific_grow_shrinks, naked?: true},
        flex_wrap: %{prefix: "flex", values: @flex_wraps},
        overflow: %{
          prefix: "overflow",
          values: @overflows
        },
        overflow_x: %{prefix: "overflow-x", values: @overflows},
        overflow_y: %{prefix: "overflow-y", values: @overflows},
        overscroll: %{
          prefix: "overscroll",
          values: @overscrolls
        },
        overscroll_x: %{prefix: "overscroll-x", values: @overscrolls},
        overscroll_y: %{prefix: "overscroll-y", values: @overscrolls},
        object_fit: %{prefix: "object", values: @object_fits},
        object_position: %{prefix: "object", values: @object_positions},
        float: %{prefix: "float", values: @floats},
        clear: %{prefix: "clear", values: @clears},
        break_after: %{prefix: "break-after", values: @break_values},
        break_before: %{prefix: "break-before", values: @break_values},
        break_inside: %{prefix: "break-inside", values: @break_inside_values},
        box_decoration: %{prefix: "box-decoration", values: @box_decoration_breaks},
        box_size: %{prefix: "box", values: @box_sizes},
        font_family: %{prefix: "font", values: @font_families},
        font_weight: %{prefix: "font", values: @font_weights},
        aspect_ratio: %{prefix: "aspect", values: @aspect_ratios},
        outline_style: %{prefix: "outline", values: @outline_styles, naked?: true},
        bg_size: %{prefix: "bg", values: @bg_sizes},
        bg_repeat: %{prefix: "bg", values: @bg_repeats},
        bg_positions: %{prefix: "bg", values: @bg_positions},
        bg_blend: %{prefix: "bg-blend", values: @bg_blends},
        bg_origin: %{prefix: "bg-origin", values: @bg_origins},
        bg_clip: %{prefix: "bg-clip", values: @bg_clips},
        bg_image: %{prefix: "bg", values: @bg_images},
        bg_attachment: %{prefix: "bg", values: @bg_attachments},
        col: %{prefix: "col", values: ~w(auto)}
      ]

      @prefixed_with_colors [
        fill_color: %{prefix: "fill"},
        outline_color: %{prefix: "outline"},
        caret_color: %{prefix: "caret"},
        accent_color: %{prefix: "accent"},
        ring_color: %{prefix: "ring"},
        shadow_color: %{prefix: "shadow"},
        ring_offset_color: %{prefix: "ring-offset"},
        divide_color: %{prefix: "divide"},
        border_color: %{prefix: "border"},
        border_color_y: %{prefix: "border-y", clears: [:border_color_t, :border_color_b]},
        border_color_x: %{prefix: "border-x", clears: [:border_color_l, :border_color_r]},
        border_color_t: %{prefix: "border-t"},
        border_color_r: %{prefix: "border-r"},
        border_color_b: %{prefix: "border-b"},
        border_color_l: %{prefix: "border-l"},
        text_decoration_color: %{prefix: "decoration"},
        bg: %{prefix: "bg"},
        text_color: %{prefix: "text"}
      ]

      @with_values [
        sr_only: %{values: ~w(sr-only not-sr-only)},
        transition_timing_function: %{
          values: @transition_timing_functions,
          arbitrary_prefix: "ease-"
        },
        text_overflows: %{values: @text_overflows},
        text_transform: %{values: @text_transforms},
        text_decoration: %{values: @text_decorations},
        isolation: %{values: @isolations},
        position: %{values: @positions},
        font_smoothing: %{values: @font_smoothings},
        font_style: %{values: @font_styles},
        display: %{values: @display},
        visibility: %{values: @visibilities},
        text_overflow: %{values: @text_overflow}
      ]

      @singletons ~w(container content-none space-y-reverse space-x-reverse divide-y-reverse divide-x-reverse ring-inset filter-none backdrop-filter-none col-auto row-auto)
                  |> Enum.concat(@font_variant_numerics)
                  |> Enum.map(&String.to_atom/1)

      @prefixed [
        stroke_width: %{prefix: "stroke"},
        rotate: %{prefix: "rotate"},
        translate: %{prefix: "translate"},
        translate_x: %{prefix: "translate-x"},
        translate_y: %{prefix: "translate-y"},
        scale_x: %{prefix: "scale-x"},
        scale_y: %{prefix: "scale-y"},
        scale: %{prefix: "scale"},
        skew_x: %{prefix: "skew-x"},
        skew_y: %{prefix: "skew-y"},
        duration: %{prefix: "duration"},
        delay: %{prefix: "delay"},
        blur: %{prefix: "blur", naked?: true},
        brightness: %{prefix: "brightness"},
        contrast: %{prefix: "contrast"},
        sepia: %{prefix: "sepia"},
        hue_rotate: %{prefix: "hue-rotate"},
        grayscale: %{prefix: "grayscale", naked?: true},
        saturate: %{prefix: "saturate"},
        invert: %{prefix: "invert", naked?: true},
        drop_shadow: %{prefix: "drop-shadow", naked?: true},
        shadow: %{prefix: "shadow", naked?: true},
        backdrop_blur: %{prefix: "backdrop-blur", naked?: true},
        backdrop_brightness: %{prefix: "backdrop-brightness"},
        backdrop_contrast: %{prefix: "backdrop-contrast"},
        backdrop_sepia: %{prefix: "backdrop-sepia"},
        backdrop_hue_rotate: %{prefix: "backdrop-hue-rotate"},
        backdrop_grayscale: %{prefix: "backdrop-grayscale", naked?: true},
        backdrop_saturate: %{prefix: "backdrop-saturate"},
        backdrop_invert: %{prefix: "backdrop-invert", naked?: true},
        backdrop_opacity: %{prefix: "backdrop-opacity"},
        border_opacity: %{prefix: "border-opacity"},
        ring_width: %{prefix: "ring", naked?: true, wont_overwrite: ~w(ring-inset), digits?: true},
        underline_offset: %{prefix: "underline-offset"},
        text_decoration_thickness: %{prefix: "decoration"},
        text_indent: %{prefix: "indent"},
        leading: %{prefix: "leading"},
        gap: %{prefix: "gap"},
        gap_x: %{prefix: "gap-x"},
        gap_y: %{prefix: "gap-y"},
        order: %{prefix: "order"},
        basis: %{prefix: "basis"},
        space_x: %{prefix: "space-x"},
        space_y: %{prefix: "space-y"},
        z: %{prefix: "z"},
        left: %{prefix: "left"},
        right: %{prefix: "right"},
        top: %{prefix: "top"},
        bottom: %{prefix: "bottom"},
        inset: %{prefix: "inset"},
        inset_y: %{prefix: "inset-y"},
        inset_x: %{prefix: "inset-x"},
        columns: %{prefix: "columns"},
        col_span: %{prefix: "col-span"},
        col_start: %{prefix: "col-start"},
        col_end: %{prefix: "col-end"},
        row_span: %{prefix: "row-span"},
        row_start: %{prefix: "row-start"},
        row_end: %{prefix: "row-end"},
        font_size: %{prefix: "text"},
        outline_width: %{prefix: "outline"},
        outline_offset: %{prefix: "outline-offset"},
        grid_cols: %{prefix: "grid-cols"},
        grid_rows: %{prefix: "grid-rows"},
        width: %{prefix: "w"},
        min_width: %{prefix: "min-w"},
        max_width: %{prefix: "max-w"},
        height: %{prefix: "h"},
        min_height: %{prefix: "min-h"},
        max_height: %{prefix: "max-h"}
      ]

      @simple_overwrite_rules %{
        "col-auto" => ~w(col_span col_start col_end)a,
        "row-auto" => ~w(row_span col_start col_end)a
      }

      @directional [
        p: %{prefix: "p", negative?: true, digits?: true},
        m: %{prefix: "m", negative?: true, digits?: true},
        rounded: %{prefix: "rounded", naked?: true},
        scroll_m: %{prefix: "scroll-m", negative?: true, digits?: true},
        scroll_p: %{prefix: "scroll-p", negative?: true, digits?: true},
        divide: %{prefix: "divide", dash_suffix?: true, values: ["reverse"], digits?: true},
        border_width: %{
          prefix: "border",
          dash_suffix?: true,
          negative?: true,
          digits?: true
        },
        border_spacing: %{
          prefix: "border-spacing",
          dash_suffix?: true,
          negative?: true,
          digits?: true
        }
      ]

      @browser_color_values ~w(
        silver gray white maroon red purple fuchsia green lime olive yellow navy blue teal aqua aliceblue
        antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood
        cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod
        darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon
        darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray
        dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray
        green greenyellow grey honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen
        lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink
        lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen
        linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue
        mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy
        oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip
        peachpuff peru pink plum powderblue purple red rosybrown royalblue saddlebrown salmon sandybrown seagreen
        seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle
        tomato turquoise violet wheat white whitesmoke yellow yellowgreen
      )

      @browser_color_prefixes ~w[# rgb( rgba( hsl( hsla(]

      @moduledoc """
      Tailwind class utilities like class merging.

      In most cases, only classes that we know all of the values for are merged.
      In some cases, we merge anything starting with a given prefix. Eventually,
      we will have tools to read your tailwind config to determine additional classes
      to merge, or have explicit configuration on additional merge logic.

      The preferred way to use tails classes the `classes/1` function. This gives you:

      1. conditional classes
      2. list merging (from left to right)
      3. arbitrary nesting

      For example:

      `classes(["mt-1 mx-2", ["pt-2": var1, "pb-4", var2], "mt-12": var3])`

      Will merge all classes from left to right, flattening the lists and conditionally
      including the classes where the associated value is true.

      ## What classes are currently merged?

      ### Directional classes

      These classes support x/y/l/r suffix merging.

      - Padding - `p`
      - Margin - `m`

      ### Only explicitly known values

      Only explicitly known values will be merged, all others will be retained.

      #{Tails.Doc.doc_with_values(@with_values)}
      #{Tails.Doc.doc_prefix_with_values(@prefixed_with_values)}
      #{Tails.Doc.doc_prefixed_with_colors(@prefixed_with_colors)}

      ### Any values matching prefix

      Any values matching the following prefixes will be merged with each other respectively

      #{Tails.Doc.doc_prefixed(@prefixed)}

      ### Singletons

      The following classes are tracked as special classes, but don't have any special merge behavior
      because they either compose with other similar classes or have no conflicts

      #{Enum.map_join(@singletons, "\n", &"* #{&1}")}
      """
      defstruct Keyword.keys(@directional) ++
                  Keyword.keys(@prefixed) ++
                  Keyword.keys(@with_values) ++
                  Keyword.keys(@prefixed_with_colors) ++
                  Keyword.keys(@prefixed_with_values) ++
                  @singletons ++
                  [
                    classes: MapSet.new(),
                    variants: %{},
                    variant: nil,
                    fallback: :default,
                    theme: :default
                  ]

      defmodule Directions do
        @moduledoc false
        defstruct [:l, :r, :t, :b, :tl, :tr, :br, :bl, :x, :y, :all]

        @type t :: %__MODULE__{
                l: String.t(),
                r: String.t(),
                t: String.t(),
                b: String.t(),
                tl: String.t(),
                tr: String.t(),
                br: String.t(),
                bl: String.t(),
                x: String.t(),
                y: String.t()
              }
      end

      @type t :: %__MODULE__{}

      @doc """
      Builds a class string out of a mixed list of inputs or a string. You can use the `~t` sigil as a shortcut.

      If the value is a string, we make a new `Tails` with it (essentially deduplicating it).

      If the value is a list, then for each item in the list:

      - If the value is a list, we call `classes/1` on it.
      - If it is a tuple, we discard it unless the second element is truthy.
      - Otherwise, we `to_string` it

      And then we merge the whole list up into one class string.

      This allows for conditional class rendering, arbitrarily nested.

      ## Examples

          iex> classes(["a", "b"])
          "a b"

          iex> classes([a: false, b: true])
          "b"

          iex> classes([[a: true, b: false], [c: false, d: true]])
          "a d"
      """
      def classes(nil), do: nil

      def classes(classes) when is_list(classes) do
        classes
        |> flatten_classes()
        |> Enum.reduce("", fn class, acc ->
          merge(acc, class)
        end)
        |> to_string()
      end

      def classes(classes) when is_binary(classes) do
        classes
        |> new()
        |> to_string()
      end

      @doc "Trims nil values and returns a map"
      def debug(value) do
        value =
          case value do
            %__MODULE__{} ->
              value

            value ->
              new(value)
          end

        value
        |> Map.from_struct()
        |> Enum.reject(&is_nil(elem(&1, 1)))
        |> Map.new()
      end

      @doc """
      Builds a class string out of a mixed list of inputs or a string.

      See `classes/1` for more information.

          iex> ~t([[a: true, b: false], [c: false, d: true]])
          "a d"
      """
      defmacro sigil_t(contents, _flags) do
        quote do
          classes([Code.eval_string(unquote(contents), []) |> elem(0)])
        end
      end

      def new(classes) do
        merge(%__MODULE__{}, classes)
      end

      defp flatten_classes(classes) do
        classes
        |> Enum.filter(fn
          {_classes, condition} ->
            condition

          _ ->
            true
        end)
        |> Enum.map(fn
          {classes, _} ->
            classes

          classes ->
            classes
        end)
        |> Enum.flat_map(fn classes ->
          if is_list(classes) do
            flatten_classes(classes)
          else
            List.wrap(classes)
          end
        end)
        |> Enum.map(&to_string/1)
      end

      @doc """
      Semantically merges two lists of tailwind classes, treating the first as a base and the second as overrides

      Generally, instead of calling `merge/2` you will call `classes/1` with a list of classes.

      See the module documentation for what classes will be merged/retained.

      Examples

          iex> merge("p-4", "p-2") |> to_string()
          "p-2"
          iex> merge("p-2", "p-4") |> to_string()
          "p-4"
          iex> merge("p-4", "px-2") |> to_string()
          "p-4 px-2"
          iex> merge("font-bold", "font-thin") |> to_string()
          "font-thin"
          iex> merge("block absolute", "fixed hidden") |> to_string()
          "fixed hidden"
          iex> merge("bg-blue-500", "bg-auto") |> to_string()
          "bg-blue-500 bg-auto"
          iex> merge("bg-auto", "bg-repeat-x") |> to_string()
          "bg-auto bg-repeat-x"
          iex> merge("bg-blue-500", "bg-red-400") |> to_string()
          "bg-red-400"
          iex> merge("grid grid-cols-2 lg:grid-cols-3", "grid-cols-3 lg:grid-cols-4") |> to_string()
          "grid grid-cols-3 lg:grid-cols-4"
          iex> merge("min-h-2", "min-h-[1rem]") |> to_string()
          "min-h-[1rem]"
          iex> merge("min-w-2", "min-h-[1rem]") |> to_string()
          "min-w-2 min-h-[1rem]"
          iex> merge("border-2", "border-gray-500") |> to_string()
          "border-2 border-gray-500"
          iex> merge("rounded-lg", "rounded") |> to_string()
          "rounded"
          iex> merge("rounded", "rounded-lg") |> to_string()
          "rounded-lg"
          iex> merge("rounded", "px-2") |> to_string()
          "px-2 rounded"
          iex> merge("border-separate", "border-spacing-1") |> to_string()
          "border-spacing-1 border-separate"
          iex> merge("shadow", "shadow-md") |> to_string()
          "shadow-md"
          iex> merge("shadow-none", "shadow-inner") |> to_string()
          "shadow-inner"
          iex> merge("shadow-lg", "shadow") |> to_string()
          "shadow"
          iex> merge("text-xl", "text-[16px]") |> to_string()
          "text-[16px]"
          iex> merge("text-white", "text-[#000]") |> to_string()
          "text-[#000]"
          iex> merge("text-center text-xl text-white", "text-[16px] text-[#000]") |> to_string()
          "text-[#000] text-center text-[16px]"
          iex> merge("border-b-4 border-opacity-20") |> to_string()
          "border-b-4 border-opacity-20"
          iex> merge("border-black border-b-4 border-opacity-20") |> to_string()
          "border-b-4 border-black border-opacity-20"
          iex> merge("border-2 border-px") |> to_string()
          "border-px"

      Classes can be removed

          iex> merge("font-normal text-black", "remove:font-normal grid") |> to_string()
          "grid text-black"

      All preceding classes can be removed

          iex> merge("font-normal text-black", "remove:* grid") |> to_string()
          "grid"

      Classes can be explicitly kept

          iex> merge("font-normal text-black", "remove:font-normal grid") |> to_string()
          "grid text-black"
      """
      def merge(tailwind, nil) when is_binary(tailwind), do: new(tailwind)
      def merge(%__MODULE__{} = tailwind, nil), do: new(tailwind)

      def merge(tailwind, classes) when is_list(tailwind) do
        merge(classes(tailwind), classes)
      end

      def merge(tailwind, classes) when is_list(classes) do
        merge(tailwind, classes(classes))
      end

      def merge(tailwind, classes) when is_binary(tailwind) do
        merge(new(tailwind), classes)
      end

      def merge(tailwind, classes) when is_binary(classes) do
        classes
        |> String.split()
        |> Enum.reduce(tailwind, &merge_class(&2, &1))
      end

      def merge(tailwind, %__MODULE__{} = classes) do
        merge(tailwind, to_string(classes))
      end

      def merge(_tailwind, value) do
        raise "Cannot merge #{inspect(value)}"
      end

      @doc "Merges a list of class strings. See `merge/2` for more"
      def merge(list) when is_list(list) do
        Enum.reduce(list, %__MODULE__{}, &merge(&2, &1))
      end

      def merge(value) when not is_list(value) do
        merge([value])
      end

      defp set_theme(tailwind, theme) do
        %{
          tailwind
          | theme: theme,
            variants:
              Map.new(tailwind.variants, fn {key, value} ->
                {key, set_theme(value, theme)}
              end)
        }
      end

      defp alter_dark_theme(%{variant: "dark"} = tailwind, fallback, theme) do
        tailwind
        |> set_theme(theme)
        |> set_fallback_theme(fallback)
      end

      defp alter_dark_theme(tailwind, fallback, theme) do
        %{
          tailwind
          | variants:
              Map.new(tailwind.variants, fn {key, value} ->
                {key, alter_dark_theme(value, fallback, theme)}
              end)
        }
      end

      defp set_fallback_theme(tailwind, fallback) do
        %{
          tailwind
          | fallback: fallback,
            variants:
              Map.new(tailwind.variants, fn {key, value} ->
                {key, set_fallback_theme(value, fallback)}
              end)
        }
      end

      for {source_class, overwrites} <- @simple_overwrite_rules do
        for overwrite <- overwrites do
          def merge_class(%{unquote(overwrite) => v} = tailwind, unquote(to_string(source_class)))
              when not is_nil(v) do
            tailwind
            |> Map.put(unquote(overwrite), nil)
            |> merge_class(unquote(to_string(source_class)))
          end
        end
      end

      @doc false
      def merge_class(tailwind, "keep:" <> class) do
        %{tailwind | classes: MapSet.put(tailwind.classes, class)}
      end

      def merge_class(%{theme: theme, variants: variants}, "remove:*") do
        %__MODULE__{
          theme: theme,
          variants:
            Map.new(variants, fn {key, value} ->
              {key, merge_class(value, "remove:*")}
            end)
        }
      end

      def merge_class(tailwind, "remove:" <> class) do
        remove(tailwind, class)
      end

      replacements =
        Enum.flat_map(@themes || [], fn {theme, replacements} ->
          replacements
          |> Tails.Custom.replacements()
          |> Enum.map(fn {key, replacement} ->
            {theme, key, replacement}
          end)
        end)

      for {theme, _replacements} <- @themes || [] do
        if @dark_themes[theme] do
          def merge_class(tailwind, "theme:" <> unquote(to_string(theme))) do
            tailwind
            |> set_theme(unquote(theme))
            |> alter_dark_theme(unquote(theme), unquote(@dark_themes[theme]))
          end
        else
          def merge_class(tailwind, "theme:" <> unquote(to_string(theme))) do
            set_theme(tailwind, unquote(theme))
          end
        end
      end

      # First match on exact theme matches
      for {theme, key, replacement} <- replacements do
        dark_replacement =
          Enum.find_value(replacements, fn {r_theme, r_key, dark_replacement} ->
            if r_theme == @dark_themes[theme] && r_key == key do
              dark_replacement
            end
          end)

        if dark_replacement do
          def merge_class(%{theme: theme} = tailwind, unquote(to_string(key)))
              when theme in [unquote(theme), unquote(to_string(theme))] do
            merge(tailwind, classes([unquote(replacement), unquote("dark:#{dark_replacement}")]))
          end
        else
          def merge_class(%{theme: theme} = tailwind, unquote(to_string(key)))
              when theme in [unquote(theme), unquote(to_string(theme))] do
            merge(tailwind, classes(unquote(replacement)))
          end
        end
      end

      # # Then match on each theme as a potential fallback theme
      # # We don't check for a matching dark theme when falling back
      for {theme, key, replacement} <- replacements do
        def merge_class(%{fallback: theme} = tailwind, unquote(to_string(key)))
            when theme in [unquote(theme), unquote(to_string(theme))] do
          merge(tailwind, classes(unquote(replacement)))
        end
      end

      # final fallback to default
      for {:default, key, replacement} <- replacements do
        dark_replacement =
          Enum.find_value(replacements, fn {r_theme, r_key, dark_replacement} ->
            if r_theme == @dark_themes[:default] && r_key == key do
              dark_replacement
            end
          end)

        if dark_replacement do
          def merge_class(tailwind, unquote(to_string(key))) do
            merge(
              tailwind,
              classes([unquote(replacement), unquote("dark:#{dark_replacement}")])
            )
          end
        else
          def merge_class(tailwind, unquote(to_string(key))) do
            merge(tailwind, classes(unquote(replacement)))
          end
        end
      end

      for class <- @no_merge_classes || [] do
        def merge_class(tailwind, unquote(class)) do
          %{tailwind | classes: MapSet.put(tailwind.classes, class)}
        end
      end

      for modifier <- @variants do
        def merge_class(tailwind, unquote(modifier) <> ":" <> rest) do
          rest = String.split(rest, ":")
          last = List.last(rest)
          variants = :lists.droplast(rest)

          key = [unquote(modifier) | variants]

          if Map.has_key?(tailwind.variants, key) do
            %{tailwind | variants: Map.update!(tailwind.variants, key, &merge_class(&1, last))}
          else
            %{
              tailwind
              | variants:
                  Map.put(tailwind.variants, key, %{new(last) | variant: Enum.join(key, ":")})
            }
          end
        end
      end

      for value <- @singletons do
        def merge_class(tailwind, unquote(to_string(value))) do
          Map.put(tailwind, unquote(value), true)
        end
      end

      for {key, %{values: values, prefix: prefix} = config} <-
            Enum.sort_by(@prefixed_with_values, fn {_, %{prefix: prefix}} ->
              -String.length(prefix)
            end) do
        if config[:naked?] do
          def merge_class(tailwind, unquote(prefix)) do
            Map.put(tailwind, unquote(key), "")
          end
        end

        unless config[:no_arbitrary?] do
          if config[:arbitrary_prefix] do
            def merge_class(
                  tailwind,
                  unquote(config[:arbitrary_prefix]) <> "-[" <> new_value
                ) do
              Map.put(tailwind, unquote(key), "[" <> new_value)
            end
          else
            def merge_class(tailwind, unquote(prefix) <> "-[" <> new_value) do
              Map.put(tailwind, unquote(key), "[" <> new_value)
            end
          end
        end

        def merge_class(tailwind, unquote(prefix) <> "-" <> new_value)
            when new_value in unquote(values) do
          Map.put(tailwind, unquote(key), new_value)
        end
      end

      @one_through_one_hundred 1..100 |> Enum.map(&to_string/1) |> Enum.take(11)

      for {key, %{prefix: prefix} = config} <-
            Enum.sort_by(@prefixed_with_colors, fn {_, %{prefix: prefix}} ->
              -String.length(prefix)
            end) do
        @clears Keyword.new(config[:clears] || [], &{&1, nil})
        def merge_class(
              tailwind,
              unquote(prefix) <> "-" <> "[currentcolor]"
            ) do
          struct(tailwind, [{unquote(key), "[currentcolor]"} | @clears])
        end

        for color_prefix <- @browser_color_prefixes do
          def merge_class(
                tailwind,
                unquote(prefix) <> "-" <> "[" <> unquote(color_prefix) <> new_value
              ) do
            struct(tailwind, [
              {unquote(key), "[#{unquote(color_prefix)}" <> new_value} | @clears
            ])
          end
        end

        for color_value <- @browser_color_values do
          match = "[#{color_value}]"

          def merge_class(tailwind, unquote(prefix) <> "-" <> unquote(match)) do
            struct(tailwind, [{unquote(key), unquote(match)} | @clears])
          end
        end

        def merge_class(tailwind, unquote(prefix) <> "-" <> new_value)
            when new_value in @all_colors do
          struct(tailwind, [{unquote(key), new_value} | @clears])
        end

        for {size, colors} <- @colors_by_size do
          def merge_class(
                tailwind,
                unquote(prefix) <>
                  "-" <> <<new_value::binary-size(unquote(size))>> <> "/" <> suffix
              )
              when new_value in unquote(colors) do
            struct(tailwind, [{unquote(key), new_value <> "/" <> suffix} | @clears])
          end
        end
      end

      for {key, %{values: values}} <- @with_values do
        def merge_class(tailwind, new_value) when new_value in unquote(values) do
          Map.put(tailwind, unquote(key), new_value)
        end
      end

      for {key, %{prefix: prefix} = config} <-
            Enum.sort_by(@prefixed, fn {_, %{prefix: prefix}} ->
              -String.length(prefix)
            end) do
        if config[:naked?] do
          def merge_class(tailwind, unquote(prefix)) do
            Map.put(tailwind, unquote(key), "")
          end
        end

        unless config[:no_arbitrary?] do
          def merge_class(tailwind, unquote(prefix) <> "-" <> "[" <> _ = value_without_suffix) do
            unquote(prefix) <> "-" <> new_value = value_without_suffix
            Map.put(tailwind, unquote(key), new_value)
          end
        end

        if config[:digits?] do
          def merge_class(tailwind, unquote(prefix) <> "-px") do
            Map.put(tailwind, unquote(key), "px")
          end

          def merge_class(tailwind, unquote(prefix) <> "-" <> <<digit::binary-size(1)>> <> rest)
              when digit in @digits do
            Map.put(tailwind, unquote(key), "#{digit}#{rest}")
          end
        else
          def merge_class(tailwind, unquote(prefix) <> "-" <> new_value) do
            Map.put(tailwind, unquote(key), new_value)
          end
        end
      end

      for {class, %{prefix: string_class} = config} <- @directional do
        @dirs %{
          x: [:r, :l],
          y: [:t, :b],
          t: [:tl, :tr],
          r: [:tr, :br],
          b: [:br, :bl],
          l: [:tl, :bl]
        }

        for {dir, clears} <- @dirs do
          string_dir =
            if config[:dash_suffix?] do
              "-" <> to_string(dir)
            else
              to_string(dir)
            end

          @clears Enum.map(clears, &{&1, nil})

          if config[:values] do
            @values config[:values]
            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-" <> value
                )
                when value in @values do
              Map.put(tailwind, unquote(class), struct(Directions, %{unquote(dir) => value}))
            end
          end

          unless config[:no_arbitrary?] do
            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-[" <> rest
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(Directions, %{unquote(dir) => "[#{rest}"})
              )
            end

            def merge_class(
                  %{unquote(class) => directions} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-[" <> rest
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(directions, [{unquote(dir), "[#{rest}"} | @clears])
              )
            end
          end

          if config[:naked?] do
            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir)
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(Directions, %{unquote(dir) => ""})
              )
            end
          end

          if config[:digits?] do
            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-px"
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(Directions, %{unquote(dir) => "px"})
              )
            end

            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-" <> <<digit::binary-size(1)>> <> rest
                )
                when digit in @digits do
              Map.put(
                tailwind,
                unquote(class),
                struct(Directions, %{unquote(dir) => "#{digit}#{rest}"})
              )
            end
          else
            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-" <> value
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(Directions, %{unquote(dir) => value})
              )
            end
          end

          if config[:negative?] do
            if config[:naked?] do
              def merge_class(
                    %{unquote(class) => nil} = tailwind,
                    "-" <>
                      unquote(string_class) <>
                      unquote(string_dir)
                  ) do
                Map.put(
                  tailwind,
                  unquote(class),
                  struct(Directions, %{unquote(dir) => "-"})
                )
              end
            end

            def merge_class(
                  %{unquote(class) => nil} = tailwind,
                  "-" <> unquote(string_class) <> unquote(string_dir) <> "-" <> value
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(Directions, %{unquote(dir) => "-" <> value})
              )
            end
          end

          if config[:values] do
            @values config[:values]
            def merge_class(
                  %{unquote(class) => directions} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <>
                    "-" <>
                    value
                )
                when value in @values do
              Map.put(
                tailwind,
                unquote(class),
                struct(directions, [{unquote(dir), value} | @clears])
              )
            end
          end

          if config[:naked?] do
            def merge_class(
                  %{unquote(class) => directions} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir)
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(directions, [{unquote(dir), ""} | @clears])
              )
            end
          end

          if config[:digits?] do
            def merge_class(
                  %{unquote(class) => directions} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-" <> <<digit::binary-size(1)>> <> rest
                )
                when digit in @digits do
              Map.put(
                tailwind,
                unquote(class),
                struct(directions, [{unquote(dir), "#{digit}#{rest}"} | @clears])
              )
            end
          else
            def merge_class(
                  %{unquote(class) => directions} = tailwind,
                  unquote(string_class) <>
                    unquote(string_dir) <> "-" <> value
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(directions, [{unquote(dir), value} | @clears])
              )
            end
          end

          if config[:negative?] do
            if config[:naked?] do
              def merge_class(
                    %{unquote(class) => directions} = tailwind,
                    "-" <> unquote(string_class) <> unquote(string_dir)
                  ) do
                Map.put(
                  tailwind,
                  unquote(class),
                  struct(directions, [{unquote(dir), "-"} | @clears])
                )
              end
            end

            def merge_class(
                  %{unquote(class) => directions} = tailwind,
                  "-" <> unquote(string_class) <> unquote(string_dir) <> "-" <> value
                ) do
              Map.put(
                tailwind,
                unquote(class),
                struct(directions, [{unquote(dir), "-" <> value} | @clears])
              )
            end
          end
        end
      end

      for {class, %{prefix: string_class} = config} <- @directional do
        if config[:values] do
          @values config[:values]
          def merge_class(
                tailwind,
                unquote(string_class) <> "-" <> value
              )
              when value in @values do
            Map.put(tailwind, unquote(class), %Directions{all: value})
          end
        end

        unless config[:no_arbitrary?] do
          def merge_class(
                tailwind,
                unquote(string_class) <> "-[" <> rest
              ) do
            Map.put(tailwind, unquote(class), %Directions{all: "[#{rest}"})
          end
        end

        if config[:negative?] do
          if config[:naked?] do
            def merge_class(tailwind, "-" <> unquote(string_class)) do
              Map.put(tailwind, unquote(class), %Directions{all: "-"})
            end
          end

          def merge_class(tailwind, "-" <> unquote(string_class) <> "-" <> value) do
            Map.put(tailwind, unquote(class), %Directions{all: "-" <> value})
          end
        end

        if config[:naked?] do
          def merge_class(tailwind, unquote(string_class)) do
            Map.put(tailwind, unquote(class), %Directions{all: ""})
          end
        end

        if config[:digits?] do
          def merge_class(
                tailwind,
                unquote(string_class) <> "-px"
              ) do
            Map.put(tailwind, unquote(class), %Directions{all: "px"})
          end

          def merge_class(
                tailwind,
                unquote(string_class) <> "-" <> <<digit::binary-size(1)>> <> rest
              )
              when digit in @digits do
            Map.put(tailwind, unquote(class), %Directions{all: "#{digit}#{rest}"})
          end
        else
          def merge_class(
                tailwind,
                unquote(string_class) <> "-" <> value
              ) do
            Map.put(tailwind, unquote(class), %Directions{all: value})
          end
        end
      end

      if @fallback_to_colors do
        for {key, %{prefix: prefix} = config} <-
              Enum.sort_by(@prefixed_with_colors, fn {_, %{prefix: prefix}} ->
                -String.length(prefix)
              end) do
          @clears Keyword.new(config[:clears] || [], &{&1, nil})

          def merge_class(tailwind, unquote(prefix) <> "-" <> value) do
            struct(tailwind, [{unquote(key), value} | @clears])
          end
        end
      end

      def merge_class(tailwind, class) do
        %{tailwind | classes: MapSet.put(tailwind.classes, class)}
      end

      @doc """
      Removes the given class from the class list.
      """
      for class <- @no_merge_classes || [] do
        def remove(tailwind, unquote(class)) do
          %{tailwind | classes: MapSet.delete(tailwind.classes, class)}
        end
      end

      for modifier <- @variants do
        def remove(tailwind, unquote(modifier) <> ":" <> rest) do
          rest = String.split(rest, ":")
          last = List.last(rest)
          variants = :lists.droplast(rest)

          key = Enum.sort([unquote(modifier) | variants])

          if Map.has_key?(tailwind.variants, key) do
            %{tailwind | variants: Map.update!(tailwind.variants, key, &remove(&1, last))}
          else
            %{
              tailwind
              | variants:
                  Map.put(tailwind.variants, key, %{new(last) | variant: Enum.join(key, ":")})
            }
          end
        end
      end

      def remove(tails, class) do
        if MapSet.member?(tails.classes, class) do
          %{tails | classes: MapSet.delete(tails.classes, class)}
        else
          # This is a bit of a hack, could maybe be optimized or fixed later

          # explicitly kept classes using `keep:` are in `classes`, so we keep them separate. The rest are classes we don't know about
          # so won't be affected by the `new/1` call below anyway.
          keep = tails.classes

          tails
          |> Map.put(:classes, MapSet.new())
          |> to_string()
          |> String.split(" ")
          |> Kernel.--([class])
          |> Enum.join(" ")
          |> new()
          |> Map.put(:classes, keep)
        end
      end

      @doc false
      def to_iodata(tailwind) do
        [
          Enum.map(@with_values, fn {key, _} ->
            simple(Map.get(tailwind, key), tailwind.variant)
          end),
          Enum.map(@directional, fn {key, %{prefix: prefix} = config} ->
            directional(Map.get(tailwind, key), prefix, !!config[:dash_suffix?], tailwind.variant)
          end),
          @singletons
          |> Enum.filter(&Map.get(tailwind, &1))
          |> Enum.map(fn class ->
            simple(to_string(class), tailwind.variant)
          end),
          Enum.map(@prefixed_with_colors, fn {key, %{prefix: prefix} = config} ->
            prefix(prefix, Map.get(tailwind, key), tailwind.variant, false)
          end),
          Enum.map(@prefixed_with_values, fn {key, %{prefix: prefix} = config} ->
            prefix(prefix, Map.get(tailwind, key), tailwind.variant, config[:naked?])
          end),
          Enum.map(@prefixed, fn {key, %{prefix: prefix} = config} ->
            prefix(prefix, Map.get(tailwind, key), tailwind.variant, config[:naked?])
          end)
        ]
        |> add_variants(tailwind)
        |> add_classes(tailwind, tailwind.variant)
      end

      defp add_variants(iodata, tailwind) do
        [
          iodata,
          tailwind.variants
          |> Kernel.||(%{})
          |> Enum.sort_by(&elem(&1, 1))
          |> Enum.map(fn {_key, value} ->
            to_iodata(value)
          end)
        ]
      end

      defp add_classes(iodata, tailwind, variant) do
        Enum.concat(
          iodata,
          case tailwind.classes do
            nil ->
              []

            "" ->
              []

            classes ->
              if Enum.empty?(classes) do
                []
              else
                if variant do
                  [" " | Enum.intersperse(Enum.map(tailwind.classes, &[variant, ":", &1]), " ")]
                else
                  [" " | Enum.intersperse(tailwind.classes, " ")]
                end
              end
          end
        )
      end

      defp simple(nil, _), do: ""
      defp simple(value, nil), do: [" ", value]
      defp simple(value, variant), do: [" ", variant, ":", value]

      defp prefix(prefix, value, variant, naked? \\ false)
      defp prefix(_prefix, nil, _, _), do: ""
      defp prefix(prefix, empty, nil, true) when empty in ["", nil], do: [" ", prefix]
      defp prefix(prefix, value, nil, _), do: [" ", prefix, "-", value]

      defp prefix(prefix, empty, variant, true) when empty in ["", nil],
        do: [" ", variant, ":", prefix]

      defp prefix(prefix, value, variant, _), do: [" ", variant, ":", prefix, "-", value]

      defp directional(nil, _key, _, _), do: ""

      defp directional(
             %Directions{
               l: l,
               r: r,
               t: t,
               b: b,
               x: x,
               y: y,
               tl: tl,
               tr: tr,
               bl: bl,
               br: br,
               all: all
             },
             key,
             dash_suffix?,
             variant
           ) do
        [
          direction(all, nil, key, variant, dash_suffix?),
          direction(tl, "tl", key, variant, dash_suffix?),
          direction(tr, "tr", key, variant, dash_suffix?),
          direction(bl, "bl", key, variant, dash_suffix?),
          direction(br, "br", key, variant, dash_suffix?),
          direction(t, "t", key, variant, dash_suffix?),
          direction(b, "b", key, variant, dash_suffix?),
          direction(l, "l", key, variant, dash_suffix?),
          direction(r, "r", key, variant, dash_suffix?),
          direction(x, "x", key, variant, dash_suffix?),
          direction(y, "y", key, variant, dash_suffix?)
        ]
        |> Enum.filter(& &1)
      end

      defp direction(nil, _, _, _, _), do: ""

      defp direction("", suffix, prefix, nil, dash_suffix?),
        do: [" ", prefix, dash_suffix(suffix, dash_suffix?)]

      defp direction("-" <> value, suffix, prefix, nil, dash_suffix?),
        do: [" -", prefix, dash_suffix(suffix, dash_suffix?), "-", value]

      defp direction(value, suffix, prefix, nil, dash_suffix?),
        do: [" ", prefix, dash_suffix(suffix, dash_suffix?), "-", value]

      defp direction("-" <> value, suffix, prefix, variant, dash_suffix?),
        do: [" ", variant, ":-", prefix, dash_suffix(suffix, dash_suffix?), "-", value]

      defp direction(value, suffix, prefix, variant, dash_suffix?),
        do: [" ", variant, ":", prefix, dash_suffix(suffix, dash_suffix?), "-", value]

      defp dash_suffix(value, true) when not is_nil(value), do: ["-", value]
      defp dash_suffix(nil, _), do: ""
      defp dash_suffix(value, _), do: value

      defimpl String.Chars do
        def to_string(tailwind) do
          tailwind
          |> to_iodata()
          |> IO.iodata_to_binary()
          |> case do
            " " <> rest -> rest
            value -> value
          end
        end

        defp to_iodata(%mod{} = tailwind) do
          mod.to_iodata(tailwind)
        end
      end

      defimpl Inspect do
        def inspect(tailwind, _opts) do
          "Tails.classes(\"#{to_string(tailwind)}\")"
        end
      end

      for {key, value} when is_binary(value) <- @colors do
        # sobelow_skip ["DOS.BinToAtom"]
        def unquote(:"#{String.replace(key, "-", "_")}")() do
          unquote(value)
        end
      end

      # for {key, value} when is_map(value) <- @colors do
      #   for {suffix, value} when is_binary(value) <- value do
      #     if suffix == "DEFAULT" do
      #       # sobelow_skip ["DOS.BinToAtom"]
      #       def unquote(:"#{String.replace(key, "-", "_")}")() do
      #         unquote(value)
      #       end
      #     else
      #       # sobelow_skip ["DOS.BinToAtom"]
      #       def unquote(:"#{String.replace(key, "-", "_")}_#{String.replace(suffix, "-", "_")}")() do
      #         unquote(value)
      #       end
      #     end
      #   end
      # end
    end
  end

  @doc false
  def replacements(replacements, prefix \\ "") do
    Enum.flat_map(replacements, fn
      {key, value} when is_binary(value) ->
        [{add_to_prefix(prefix, key), value}]

      {key, value} when is_list(value) ->
        replacements(value, add_to_prefix(prefix, key))
    end)
  end

  defp add_to_prefix("", value), do: to_string(value)
  defp add_to_prefix(prefix, value), do: to_string(prefix) <> "-" <> to_string(value)
end