defmodule TailColors do
@moduledoc """
Helper functions for working with tailwind classes
"""
@colors [
"slate",
"gray",
"zinc",
"neutral",
"stone",
"red",
"orange",
"amber",
"yellow",
"lime",
"green",
"emerald",
"teal",
"cyan",
"sky",
"blue",
"indigo",
"violet",
"purple",
"fuchsia",
"pink",
"rose"
] ++ (Application.compile_env(:tail_colors, :colors) || [])
@tints [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
@themed_colors Application.compile_env(:tail_colors, :themed_colors) || %{}
@doc ~S"""
substitutes a themed color from config for the actual color name.
"""
def theme(class_str) do
Map.keys(@themed_colors)
|> Enum.map(&Atom.to_string/1)
|> Enum.reduce(class_str, fn item, new_str ->
String.replace(new_str, item, Map.get(@themed_colors, String.to_atom(item)))
end)
end
@doc ~S"""
takes list of classNames and starting text, and finds first instance
that matches with a known color and tint, a default value can also be set
## Examples
iex> TailColors.get_color("thing text-red-400 something", "text")
"text-red-400"
iex> TailColors.get_color("thing bg-blue", "bg")
"bg-blue"
iex> TailColors.get_color("thing else", "bg")
nil
iex> TailColors.get_color("thing bg-monster else", "bg")
nil
iex> TailColors.get_color("thing bg-blue-404 else", "bg")
nil
iex> TailColors.get_color("thing else", "bg", "bg-blue-600")
"bg-blue-600"
"""
def get_color(classes, lookup, default \\ nil)
def get_color(classes, p, d) when is_bitstring(classes), do: get_color(break(classes), p, d)
def get_color(classes, prefix, default) when is_bitstring(prefix) do
classes
|> Enum.find(fn class -> String.starts_with?(class, prefix <> "-") end)
|> color_tint(prefix)
|> case do
nil -> default
match -> match
end
end
@doc ~S"""
takes list of classNames and prefix text or list of options, and finds first instance that matches,
a default value can also be set
## Examples
iex> TailColors.get("thing rounded something", "rounded")
"rounded"
iex> TailColors.get("thing rounded-xl something", "rounded")
"rounded-xl"
iex> TailColors.get("thing else", "rounded")
nil
iex> TailColors.get("thing else", "rounded", "rounded-md")
"rounded-md"
iex> TailColors.get("thing box else", ["circle", "rounded", "box"])
"box"
iex> TailColors.get("thing else", ["circle", "rounded", "box"])
nil
iex> TailColors.get("thing else", ["circle", "rounded", "box"], "rounded")
"rounded"
"""
def get(classes, lookup, default \\ nil)
def get(classes, prefix, d) when is_bitstring(classes), do: get(break(classes), prefix, d)
def get(classes, prefix, default) when is_bitstring(prefix) do
if prefix in classes do
prefix
else
case first_prefix(classes, prefix) do
nil -> default
match -> match
end
end
end
def get(classes, list, default) when is_list(list) do
case common_items(classes, list) do
nil -> default
[h | _] -> h
end
end
defp first_prefix(cl, prefix), do: Enum.find(cl, &String.starts_with?(&1, prefix <> "-"))
@doc ~S"""
takes a list of classNames and matches the first class with the prefix and returns the match or default values
## Examples
iex> TailColors.get("thing text-red-400 something", "text", "blue", 600)
"text-red-400"
iex> TailColors.get("thing text-red something", "text", "blue", 600)
"text-red-600"
iex> TailColors.get("thing something", "text", "blue", 600)
"text-blue-600"
"""
def get(class_str, p, c, t)
def get(class_str, p, c, t) when is_bitstring(class_str),
do: get(break(class_str), p, c, t)
def get(class_list, prefix, default_color, default_tint) do
case get(class_list, prefix) do
nil ->
"#{prefix}-#{default_color}-#{default_tint}"
match ->
if int_ending?(match) do
match
else
"#{match}-#{default_tint}"
end
end
end
defp color_tint(nil, _), do: nil
defp color_tint(class, prefix) do
cond do
!is_color?(class, prefix) -> nil
int_ending?(class) and !is_tint?(class) -> nil
true -> class
end
end
defp is_color?(class, prefix) do
case String.split(class, "-") do
[^prefix, color] -> color in @colors
[^prefix, color, _] -> color in @colors
_ -> false
end
end
defp int_ending?(c), do: String.match?(c, ~r/.*-\d+/)
defp is_tint?(class) do
with [str | _] <- String.split(class, "-") |> Enum.reverse(),
{int, _} <- Integer.parse(str) do
int in @tints
else
_ -> false
end
end
defp break(class_list), do: String.split(class_list, ~r/\s+/)
@doc ~S"""
takes a list of classNames and a string, and returns true if the string is in the list
## Examples
iex> TailColors.has?("thing text-red-400 something", "something")
true
iex> TailColors.has?("thing bg-blue", "something")
false
"""
def has?(classes, str) when is_bitstring(classes),
do: has?(break(classes), str)
def has?(classes, str) when is_bitstring(str) do
classes
|> Enum.any?(&String.starts_with?(&1, str))
end
@doc ~S"""
takes a list of classNames and finds any colors that appear that are not prefixed by a string followed by a -
returns a tuple of {color, tint}
## Examples
iex> TailColors.main_color("thing red something", "blue", 700)
{"red", 700}
iex> TailColors.main_color("thing red-400 something", "blue", 700)
{"red", 400}
iex> TailColors.main_color("thing something", "blue", 700)
{"blue", 700}
"""
def main_color(classes, c, t) when is_bitstring(classes), do: main_color(break(classes), c, t)
def main_color(classes, default_color, default_tint) when is_list(classes) do
case(common_items(classes, @colors)) do
nil ->
case with_tints(classes, default_tint) do
nil -> {default_color, default_tint}
tuple -> tuple
end
match ->
color = match |> hd
{color, default_tint}
end
end
defp with_tints(classes, default_tint) do
classes
|> Enum.map(&parse_color_tint(&1, default_tint))
|> Enum.find(fn
{_color, nil} -> false
{color, tint} -> color in @colors and tint in @tints
end)
end
defp parse_color_tint(nil, _), do: nil
defp parse_color_tint(class, default_tint) do
class
|> String.split("-")
|> Enum.reverse()
|> case do
[h | []] ->
{h, nil}
[h | t] ->
if str_to_int(h) in @tints do
{t |> Enum.reverse() |> Enum.join("-"), String.to_integer(h)}
else
{[h | t] |> Enum.reverse() |> Enum.join("-"), default_tint}
end
end
end
defp str_to_int(str) do
case Integer.parse(str) do
{int, _} -> int
_ -> nil
end
end
defp common_items(l1, l2) do
l3 = l1 -- l2
(l1 -- l3)
|> case do
[] -> nil
match -> match
end
end
defp modify(class_str, mod_fun, args \\ []) do
if is_tint?(class_str) do
tint = get_tint(class_str)
tint = apply(__MODULE__, mod_fun, [tint] ++ args)
replace_tint(class_str, tint)
else
class_str
end
end
defp replace_tint(c, tint) when is_integer(tint), do: replace_tint(c, Integer.to_string(tint))
defp replace_tint(class_str, tint), do: Regex.replace(~r/-\d+$/, class_str, "-" <> tint)
defp get_tint(str),
do: str |> String.split("-") |> Enum.reverse() |> hd() |> String.to_integer()
@doc ~S"""
moves the tint down the list of tints.
## Examples
iex> TailColors.darker(600, 1)
700
iex> TailColors.darker(600, 3)
900
iex> TailColors.darker(600, 9)
950
iex> TailColors.darker("text-green-400", 1)
"text-green-500"
iex> TailColors.darker("text-green-400", 9)
"text-green-950"
iex> TailColors.darker("not-a-tint", 1)
"not-a-tint"
"""
def darker(class_str, steps) when is_bitstring(class_str),
do: modify(class_str, :darker, [steps])
def darker(tint, steps), do: step(tint, steps)
@doc ~S"""
moves the tint up the list of tints.
## Examples
iex> TailColors.lighter(600, 1)
500
iex> TailColors.lighter(600, 3)
300
iex> TailColors.lighter(600, 9)
50
iex> TailColors.lighter("text-green-400", 1)
"text-green-300"
iex> TailColors.lighter("text-green-400", 9)
"text-green-50"
iex> TailColors.lighter("not-a-tint", 1)
"not-a-tint"
"""
def lighter(class_str, steps) when is_bitstring(class_str),
do: modify(class_str, :lighter, [steps])
def lighter(tint, steps), do: step(tint, -1 * steps)
defp step(tint, steps) when is_integer(tint) and is_integer(tint) do
{_t, index} =
Enum.with_index(@tints)
|> Enum.find(fn {t, _i} -> t == tint end)
cond do
index + steps < 0 ->
hd(@tints)
true ->
case Enum.fetch(@tints, index + steps) do
{:ok, tint} -> tint
:error -> List.last(@tints)
end
end
end
@doc ~S"""
takes a list of classNames and a list of classes to remove, and then removes those classes if they appear in the classNames
## Examples
iex> TailColors.clean("thing needle something", "needle")
"thing something"
iex> TailColors.clean("thing something", "needle")
"thing something"
iex> TailColors.clean("thing needle haystack something", "needle haystack")
"thing something"
"""
def clean(class_list, remove_list)
def clean(cl, rl) when is_bitstring(cl), do: clean(break(cl), rl)
def clean(cl, rl) when is_bitstring(rl), do: clean(cl, break(rl))
def clean(class_list, remove_list) do
(class_list -- remove_list) |> Enum.filter(& &1) |> Enum.join(" ")
end
@doc ~S"""
takes a list of classNames and a list of classes to remove, and then removes those classes if they appear in the classNames
## Examples
iex> TailColors.clean_prefix("thing bg-blue-500 something", "bg")
"thing something"
iex> TailColors.clean_prefix("thing something", "bg")
"thing something"
iex> TailColors.clean_prefix("thing bg-green-200 text-red-600 something", "bg text")
"thing something"
"""
def clean_prefix(class_list, remove_list)
def clean_prefix(cl, rl) when is_bitstring(cl), do: clean_prefix(break(cl), rl)
def clean_prefix(cl, rl) when is_bitstring(rl), do: clean_prefix(cl, break(rl))
def clean_prefix(class_list, remove_list) do
class_list
|> Enum.filter(fn class ->
!Enum.any?(remove_list, fn remove_class ->
String.starts_with?(class, remove_class <> "-")
end)
end)
|> Enum.join(" ")
end
@doc ~S"""
takes a list of classNames and removes any colors that appear that do not follow the tailwind color struction prefix-color-tint
#
## Examples
iex> TailColors.clean_colors("thing blue something")
"thing something"
iex> TailColors.clean_colors("thing something")
"thing something"
iex> TailColors.clean_colors("thing blue red something")
"thing something"
"""
def clean_colors(class_list)
def clean_colors(c) when is_bitstring(c), do: clean_colors(break(c))
def clean_colors(class_list) do
class_list
|> Enum.filter(fn class ->
!Enum.any?(@colors, fn color -> String.starts_with?(class, color <> "") end)
end)
|> Enum.join(" ")
end
@doc ~S"""
takes a either a tint, or a color class, and returns a tint that is easier to read on that background
#
## Examples
iex> TailColors.invert(200)
600
iex> TailColors.invert("bg-blue-600")
"bg-blue-50"
iex> TailColors.invert(nil)
nil
iex> TailColors.invert("weirdo")
"weirdo"
"""
def invert(class_str)
def invert(str) when is_bitstring(str), do: modify(str, :invert)
def invert(nil), do: nil
def invert(50), do: 400
def invert(100), do: 500
def invert(200), do: 600
def invert(300), do: 700
def invert(400), do: 50
def invert(500), do: 50
def invert(600), do: 50
def invert(700), do: 100
def invert(800), do: 100
def invert(900), do: 200
def invert(950), do: 300
end