defmodule InlineSVG do @moduledoc """ Render inline SVG. ## Initialization ```elixir def SVGHelper do use InlineSVG, root: "assets/static/svg", default_collection: "generic" end ``` This will generate functions for each SVG file, effectively caching them at compile time. ## Usage ### render SVG from default collection ```elixir svg("home") ``` It will load the SVG file from `assets/static/svg/generic/home.svg`: ```html ... ``` ### render SVG from other collections You can break up SVG files into collections, and use the second argument of `svg/2` to specify the name of collection: ```elixir svg("user", "fontawesome") ``` It will load the SVG file from `assets/static/svg/fontawesome/user.svg`: ```html ... ``` ### render SVG with custom HTML attributes You can also pass optional HTML attributes into the function to set those attributes on the SVG: ```elixir svg("home", class: "logo", id: "bounce-animation") svg("home", "fontawesome", class: "logo", id: "bounce-animation") ``` It will output: ```html ``` ## Options There are several configuration options for meeting your needs. ### `:root` Specify the directory from which to load SVG files. You must specify it by your own. ### `:function_prefix` Specify the prefix of functions. By the default, the value is `""`. The generated function name is `svg`. If this value is "_". Then generated function name is `_svg`. ### `:default_collection` Specify the default collection to use. The deafult value is `generic`. ## Use in Phoenix An example: ```elixir def DemoWeb.SVGHelper do use InlineSVG, root: "assets/static/svg", function_prefix: "_", default_collection: "generic" def svg(arg1) do Phoenix.HTML.raw(_svg(arg1)) end def svg(arg1, arg2) do Phoenix.HTML.raw(_svg(arg1, arg2)) end def svg(arg1, arg2, arg3) do Phoenix.HTML.raw(_svg(arg1, arg2, arg3)) end end ``` """ alias InlineSVG.HTML @doc """ The macro precompiles the SVG files into functions. """ defmacro __using__(opts \\ []) do root = Keyword.fetch!(opts, :root) {root, _} = Code.eval_quoted(root) if !File.dir?(root) do raise "invalid :root option" end function_prefix = Keyword.get(opts, :function_prefix, "") default_collection = Keyword.get(opts, :default_collection, "generic") [recompile_hooks(root) | generate_svg_fns(root, function_prefix, default_collection)] end # Trigger recompile when SVG files change. # Read more at https://hexdocs.pm/mix/1.13/Mix.Tasks.Compile.Elixir.html defp recompile_hooks(root) do quote bind_quoted: [root: root] do @root root paths = @root |> Path.join("**/*.svg") |> Path.wildcard() |> Enum.filter(&File.regular?(&1)) @paths_hash :erlang.md5(paths) for path <- paths do @external_resource path end def __mix_recompile__?() do @root |> Path.join("**/*.svg") |> Path.wildcard() |> Enum.filter(&File.regular?(&1)) |> :erlang.md5() != @paths_hash end end end defp generate_svg_fns(root, function_prefix, default_collection) do root |> scan_svgs() |> Enum.flat_map(&cache_svg(&1, function_prefix, default_collection)) end defp scan_svgs(root) do root |> Path.join("**/*.svg") |> Path.wildcard() |> Stream.filter(&File.regular?(&1)) |> Enum.map(fn svg_path -> [collection_name, svg_name] = svg_path |> Path.relative_to(root) |> Path.rootname() |> String.split("/", parts: 2) {collection_name, svg_name, svg_path} end) end defp cache_svg({collection, name, path}, function_prefix, default_collection) do content = read_svg(path) # parse HTML at compile time. parsed_html = content |> HTML.parse_html!() |> Macro.escape() generic_functions = if collection == default_collection do quote do def unquote(:"#{function_prefix}svg")(unquote(name)) do unquote(:"#{function_prefix}svg")(unquote(name), unquote(collection), []) end def unquote(:"#{function_prefix}svg")(unquote(name), opts) when is_list(opts) do unquote(:"#{function_prefix}svg")(unquote(name), unquote(collection), opts) end end end explicit_functions = quote do def unquote(:"#{function_prefix}svg")(unquote(name), unquote(collection)) do unquote(:"#{function_prefix}svg")(unquote(name), unquote(collection), []) end def unquote(:"#{function_prefix}svg")(unquote(name), unquote(collection), []) do unquote(content) end def unquote(:"#{function_prefix}svg")(unquote(name), unquote(collection), opts) do unquote(parsed_html) |> HTML.insert_attrs(opts) |> HTML.to_html() end end [generic_functions, explicit_functions] end defp read_svg(path) do path |> File.read!() |> String.trim() end end