Mix Task: heexify.ex
defmodule Mix.Tasks.App.Heexify do
@moduledoc """
Used to convert static SVGs to Heex components.
command: `mix app.heexify`
Inspired by: PetalFramework
Git: https://github.com/petalframework/petal_components/blob/main/lib/mix/tasks/heroicons/generate.ex
"""
@shortdoc "Used to enable usage of SVGs in project"
@app_name "DerpyCoder"
@svg_path "priv/static/svgs/"
use Mix.Task
alias Phoenix.Naming
@impl Mix.Task
def run(_) do
Mix.Task.run("app.start")
crunch_svgs_folder()
end
# ==============================================================================
# It Crunches SVGs and for that, it:
# - Goes over each file in SVGs folder.
# - Segments modules based on folder structure by building hashmap.
# - Assembles the module with helper functions.
# - Writes them into Separate files.
# ==============================================================================
defp crunch_svgs_folder() do
@svg_path <> "**/*.svg"
|> Path.wildcard()
|> Enum.map(&String.replace(&1, @svg_path, ""))
|> Enum.reduce(%{}, &build_hashmap/2)
|> Enum.map(&assemble_module/1)
|> Enum.each(&write_module/1)
end
# ==============================================================================
# Hashmap
# It goes over array of paths, which looks like:
# ["hero-icons/solid/eye", "hero-icons/solid/code"]
#
# And groups them based on immediate containing folder.
# Which looks like:
# %{
# "hero-icons/solid => ["hero-icons/solid/eye", "hero-icons/solid/code"]
# }
#
# This helps us with colocating each file inside their respective modules.
# So the key in above hashmap becomes the module: HeroIcons.Solid
# And the value becomes the HEEX Component: HeroIcons.Solid.code
# ==============================================================================
defp build_hashmap(path, acc) do
value = case Map.get(acc, Path.dirname(path)) do
nil -> [path]
array -> [path | array]
end
Map.put(acc, Path.dirname(path), value)
end
# ==============================================================================
# Module Assembler
# It creates the file structure for the Module.
# Adds sensible commments with examples.
# And assimilates all the svgs withing as functions.
# ==============================================================================
defp assemble_module({module_path, file_paths}) do
module_name = module_name(module_path)
module = """
defmodule #{module_name} do
@moduledoc \"\"\"
# #{module_name}
Contains Heexified SVG Components, to ease usage of SVG without cluttering markup.
## Examples:
<#{module_name}.svg class="w-5 h-5" />
<#{module_name}.svg class="w-5 h-5" title="Accessible Title" />
\"\"\"
use Phoenix.Component
import #{@app_name}Web.SVG
# coveralls-ignore-start
#{assimilate_svg(file_paths)}
# coveralls-ignore-stop
end
"""
{module_path, module}
end
# ==============================================================================
# Module Name Helper
# ==============================================================================
defp module_name(module_path) do
module_path
|> String.replace("-", "_")
|> String.split("/")
|> Enum.map(&Naming.camelize/1)
|> case do
nil -> ""
module_name -> Enum.join(module_name, ".")
end
end
# ==============================================================================
# Loops over each file.
# And Creates HEEX Components array.
# ==============================================================================
defp assimilate_svg(file_paths) do
file_paths
|> Enum.map(&create_component/1)
|> Enum.join("\n")
end
# ==============================================================================
# Creates HEEX Component.
# Reads SVG Files.
# Alters and Assemble it.
# ==============================================================================
defp create_component(file_path) do
File.read!(@svg_path <> file_path)
|> alter_svg()
|> assemble_component(file_path)
end
# ==============================================================================
# Trims and adds some new lines to beautify the svg.
# Also adds the ability to pass in class, title and any attributes to the SVG.
# ==============================================================================
defp alter_svg(svg) do
svg
|> String.trim()
|> String.replace(~r/<svg /, "<svg class={@class} {@extra} ")
|> String.replace(~r/\"><path/, "\">\n <.title title={@title} />\n <path")
|> String.replace(~r/><path/, ">\n <path")
|> String.replace(~r/<\/svg/, "\n <\/svg")
end
# ==============================================================================
# Assembles the HEEX Component.
# Adds Comments and Assigns to create flexible HEEX Component.
# ==============================================================================
defp assemble_component(svg, file_path) do
module_name =
file_path
|> Path.dirname()
|> module_name()
function_name = function_name(file_path)
"""
@doc \"\"\"
# #{module_name}.#{function_name}
A Heexified SVG component, that can be passed class, title and extra attributes, to alter it.
## Examples:
<#{module_name}.#{function_name} class="w-5 h-5" />
<#{module_name}.#{function_name} class="w-5 h-5" title="Accessible Title" />
\"\"\"
def #{function_name}(assigns) do
assigns = assigns
|> assign_new(:title, fn -> nil end)
|> assign_new(:class, fn -> nil end)
|> assign_new(:extra, fn -> assigns_to_attributes(assigns, ~w(class)a) end)
~H\"\"\"
#{svg}
\"\"\"
end
"""
end
# ==============================================================================
# Function Name Helper
# ==============================================================================
defp function_name(file_path) do
file_path
|> Path.basename(".svg")
|> String.replace("-", "_")
end
# ==============================================================================
# Writes the Assembled Module to file.
# ==============================================================================
defp write_module({module_path, module}) do
module_path = String.replace(module_path, "-", "_")
destination = "lib/#{Naming.underscore(@app_name)}_web/components/_svg/#{module_path}.ex"
unless File.exists?(destination) do
File.mkdir_p(Path.dirname(destination))
end
File.write!(destination, module)
end
end
Helper Component: svg.ex
defmodule DerpyCoderWeb.SVG do
@moduledoc """
SVG helper components.
"""
use Phoenix.Component
def title(assigns) do
~H"""
<%= if not is_nil(@title) do %>
<title><%= @title %></title>
<% end %>
"""
end
end
Input: priv/static/svgs/hero-icons/solid/eye.svg
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0z" clip-rule="evenodd"/></svg>
Input: priv/static/svgs/hero-icons/solid/code.svg
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M12.316 3.051a1 1 0 0 1 .633 1.265l-4 12a1 1 0 1 1-1.898-.632l4-12a1 1 0 0 1 1.265-.633zM5.707 6.293a1 1 0 0 1 0 1.414L3.414 10l2.293 2.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 0zm8.586 0a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 1 1-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 0 1 0-1.414z" clip-rule="evenodd"/></svg>
Output: lib/derpy_coder_web/components/_svg/hero_icons/solid.ex
defmodule HeroIcons.Solid do
@moduledoc """
# HeroIcons.Solid
Contains Heexified SVG Components, to ease usage of SVG without cluttering markup.
## Examples:
<HeroIcons.Solid.svg class="w-5 h-5" />
<HeroIcons.Solid.svg class="w-5 h-5" title="Accessible Title" />
"""
use Phoenix.Component
import DerpyCoderWeb.SVG
# coveralls-ignore-start
@doc """
# HeroIcons.Solid.eye
A Heexified SVG component, that can be passed class, title and extra attributes, to alter it.
## Examples:
<HeroIcons.Solid.eye class="w-5 h-5" />
<HeroIcons.Solid.eye class="w-5 h-5" title="Accessible Title" />
"""
def eye(assigns) do
assigns = assigns
|> assign_new(:title, fn -> nil end)
|> assign_new(:class, fn -> nil end)
|> assign_new(:extra, fn -> assigns_to_attributes(assigns, ~w(class)a) end)
~H"""
<svg class={@class} {@extra} xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<.title title={@title} />
<path d="M10 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0z" clip-rule="evenodd"/>
</svg>
"""
end
@doc """
# HeroIcons.Solid.code
A Heexified SVG component, that can be passed class, title and extra attributes, to alter it.
## Examples:
<HeroIcons.Solid.code class="w-5 h-5" />
<HeroIcons.Solid.code class="w-5 h-5" title="Accessible Title" />
"""
def code(assigns) do
assigns = assigns
|> assign_new(:title, fn -> nil end)
|> assign_new(:class, fn -> nil end)
|> assign_new(:extra, fn -> assigns_to_attributes(assigns, ~w(class)a) end)
~H"""
<svg class={@class} {@extra} xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<.title title={@title} />
<path fill-rule="evenodd" d="M12.316 3.051a1 1 0 0 1 .633 1.265l-4 12a1 1 0 1 1-1.898-.632l4-12a1 1 0 0 1 1.265-.633zM5.707 6.293a1 1 0 0 1 0 1.414L3.414 10l2.293 2.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 0zm8.586 0a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 1 1-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 0 1 0-1.414z" clip-rule="evenodd"/>
</svg>
"""
end
# coveralls-ignore-stop
end