Skip to content

Instantly share code, notes, and snippets.

@aiwaiwa
Last active March 10, 2023 15:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aiwaiwa/8e9d54d2cb929370f41a9715292fac6d to your computer and use it in GitHub Desktop.
Save aiwaiwa/8e9d54d2cb929370f41a9715292fac6d to your computer and use it in GitHub Desktop.
heroicons_gen for Phoenix >=1.7.1
# |
# | This is designed for Phoenix >= 1.7.1 to add Heroicons components
# | from the list of .svg files in any of:
# |
possible_sources = [
# As introduced in =1.7.1
"priv/hero_icons/optimized/20/solid",
# Since >=1.7.2 with a maybe variant
"assets/vendor/hero_icons/optimized/20/solid/",
"assets/vendor/heroicons/optimized/20/solid/"
]
class_name_prefix = "heroicon-"
#
# Add this script to lib/components or lib/components/heroicons folder, according to your taste.
#
# Call this script with from any location, as long as the path to .exs file is reachable.
# The script will discover the closest mix.exs to the script.
#
# $ eilixir heroicons_gen.exs
#
# This will generate heroicons.ex in the folder where heroicons_gen.exs has been dropped in.
#
defmodule Traverse do
def locate(path, where) do
cond do
File.regular?(path) ->
case where.(path) do
false ->
[]
true ->
[path]
end
File.dir?(path) ->
File.ls!(path)
|> Enum.map(&Path.join(path, &1))
|> Enum.map(&locate(&1, where))
|> Enum.concat()
true ->
[]
end
end
def find_up(path, match) when is_function(match, 1) do
path = Path.absname(path)
false = File.regular?(path)
case path do
"" ->
""
path ->
case File.ls!(path) |> Enum.filter(&match.(&1)) do
[_ | _] -> path
[] -> find_up(Path.dirname(path), match)
end
end
end
end
path = __DIR__
mix_root = Traverse.find_up(path, fn v -> v == "mix.exs" end)
mix_root =
case mix_root do
"" -> raise("mix.exs folder not located from #{Path.absolute(path)}")
_ -> mix_root
end
IO.inspect(mix_root, label: "mix.exs")
possible_sources_from_mix_root = Enum.map(possible_sources, &Path.join(mix_root, &1))
# Detect ModuleWeb
w =
Traverse.locate(mix_root, fn f ->
String.contains?(f, "/core_components.ex") and
!String.starts_with?(f, Path.join(mix_root, "_build")) and
!String.starts_with?(f, Path.join(mix_root, ".elixir_ls")) and
!String.starts_with?(f, Path.join(mix_root, "deps"))
end)
1 = length(w)
[core_components | _] = w
IO.inspect(core_components, label: "core_components")
r = File.read!(core_components)
r = String.split(r, "\n")
[first_line | _] = r
web_module =
String.trim_trailing(String.trim_leading(first_line, "defmodule "), ".CoreComponents do")
IO.inspect(web_module, label: "web module name")
true = web_module != ""
contains_any = fn search, enum ->
Enum.any?(enum, &String.contains?(search, &1))
end
icons =
Traverse.locate(mix_root, fn f ->
contains_any.(f, possible_sources_from_mix_root) and Path.extname(f) == ".svg"
end)
|> Enum.map(fn f -> Path.basename(f) end)
|> Enum.uniq()
case length(icons) do
0 -> raise("This doesn't look like a Phoenix >=1.7.1 folder structure")
_ -> nil
end
f = Path.join(__DIR__, "heroicons.ex")
stream = File.stream!(f, [:write, :utf8])
_icon_def = fn f ->
basename = Path.basename(f)
trimmed = String.trim_trailing(basename, Path.extname(f))
trimmed_underlined = String.replace(trimmed, "-", "_")
"""
attr :rest, :global,
doc: "the arbitrary HTML attributes for the svg container",
include: ~w(fill stroke stroke-width)
attr :outline, :boolean, default: true
attr :solid, :boolean, default: false
attr :mini, :boolean, default: false
@doc \"\"\"
#{basename}
\"\"\"
def #{trimmed_underlined}(%{solid: true} = assigns) do
~H\"\"\"
<.icon name={"#{class_name_prefix}#{trimmed}-solid"} {@rest}/>
\"\"\"
end
def #{trimmed_underlined}(%{mini: true} = assigns) do
~H\"\"\"
<.icon name={"#{class_name_prefix}#{trimmed}-mini"} {@rest}/>
\"\"\"
end
def #{trimmed_underlined}(assigns) do
~H\"\"\"
<.icon name={"#{class_name_prefix}#{trimmed}"} {@rest}/>
\"\"\"
end
"""
end
span_def = fn f ->
basename = Path.basename(f)
trimmed = String.trim_trailing(basename, Path.extname(f))
trimmed_underlined = String.replace(trimmed, "-", "_")
"""
attr :class, :string, default: "w-5 h-5"
attr :rest, :global,
doc: "the arbitrary HTML attributes for the svg container",
include: ~w(fill stroke stroke-width)
attr :outline, :boolean, default: true
attr :solid, :boolean, default: false
attr :mini, :boolean, default: false
@doc \"\"\"
#{basename}
\"\"\"
def #{trimmed_underlined}(%{solid: true} = assigns) do
~H\"\"\"
<span class={["#{class_name_prefix}#{trimmed}-solid", @class]} {@rest}/>
\"\"\"
end
def #{trimmed_underlined}(%{mini: true} = assigns) do
~H\"\"\"
<span class={["#{class_name_prefix}#{trimmed}-mini", @class]} {@rest}/>
\"\"\"
end
def #{trimmed_underlined}(assigns) do
~H\"\"\"
<span class={["#{class_name_prefix}#{trimmed}", @class]} {@rest}/>
\"\"\"
end
"""
end
[
"""
defmodule #{web_module}.Heroicons do
use Phoenix.Component
import #{web_module}.CoreComponents, warn: false
"""
| [Stream.map(icons, &span_def.(&1)) |> Enum.to_list() | ["end"]]
]
|> Stream.into(stream)
|> Stream.run()
IO.inspect(length(icons), label: "icons")
IO.inspect(f, label: "output")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment