defmodule FrancisHtmx do @moduledoc """ Provides a macro to render htmx content by loading htmx.js. Also provides a sigil to render EEx content similar to ~H from Phoenix.LiveView Usage: ```elixir defmodule Example do use Francis import FrancisHtmx htmx(fn _conn -> assigns = %{} ~E\"\"\"

Color Swap Demo

\"\"\" end) get("/colors", fn _ -> new_color = 3 |> :crypto.strong_rand_bytes() |> Base.encode16() |> then(&"\#{&1}") assigns = %{new_color: new_color} ~E\"\"\"

"> Color Swap Demo

\"\"\" end) end ``` In this scenario we are loading serving an HTML that has the htmx.js library loaded and serves the root content given by htmx/1 """ defmacro __using__(opts) do quote do import FrancisHtmx import unquote(__MODULE__), only: [htmx: 1, htmx: 2, sigil_E: 2] checker = ~r/^(\d+\.)?(\d+\.)?(\*|\d+)$/ version = Application.compile_env(:francis_htmx, :version, "2") version = Keyword.get(unquote(opts), :version, version) title = Keyword.get(unquote(opts), :title, "") head = Keyword.get(unquote(opts), :head, "") if !Regex.match?(checker, version) do raise "Invalid version format. Expected format is 'x.y.z' or 'x.y.*'. Got: '#{version}'" end Module.put_attribute(__MODULE__, :htmx_version, version) Module.put_attribute(__MODULE__, :htmx_title, title) Module.put_attribute(__MODULE__, :htmx_head, head) Module.register_attribute(__MODULE__, :htmx_version, accumulate: false) Module.register_attribute(__MODULE__, :htmx_title, accumulate: false) Module.register_attribute(__MODULE__, :htmx_head, accumulate: false) end end @doc """ Renders htmx content by loading htmx.js and rendering binary content. """ @spec htmx((Plug.Conn.t() -> binary())) :: Macro.t() defmacro htmx(content) do quote location: :keep do get("/", fn conn -> html(conn, """ #{@htmx_head} #{@htmx_title} #{unquote(content).(conn)} """) end) end end @doc """ Renders htmx content by loading htmx.js and rendering binary content. """ @spec htmx((Plug.Conn.t() -> binary()), Keyword.t()) :: Macro.t() defmacro htmx(content, opts) do quote location: :keep do get("/", fn conn -> title = Keyword.get(unquote(opts), :title, @htmx_title) head = Keyword.get(unquote(opts), :head, @htmx_head) html(conn, """ #{head} #{title} #{unquote(content).(conn)} """) end) end end @doc """ Provides a sigil to render EEx content similar to ~H from Phoenix.LiveView If a variable named "assigns" doesn't exist, it will be set to an empty map. """ @spec sigil_E(String.t(), Keyword.t()) :: Macro.t() defmacro sigil_E(content, _opts \\ []) do if Macro.Env.has_var?(__CALLER__, {:assigns, nil}) do quote location: :keep do content = EEx.eval_string(unquote(content), [assigns: var!(assigns)], engine: Phoenix.HTML.Engine) content |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() end else quote location: :keep do assigns = %{} content = EEx.eval_string(unquote(content), [assigns: assigns], engine: Phoenix.HTML.Engine) content |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() end end end end