lib/francis_htmx.ex

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\"\"\"
        <style>
          .smooth {   transition: all 1s ease-in; font-size: 8rem; }
        </style>
        <div hx-get="/colors" hx-trigger="every 1s">
          <p id="color-demo" class="smooth">Color Swap Demo</p>
        </div>
        \"\"\"
      end)

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

        ~E\"\"\"
        <p id="color-demo" class="smooth" style="<%= "color:\#{@new_color}"%>">
        Color Swap Demo
        </p>
        \"\"\"
      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
  """
  @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
    title = Keyword.get(opts, :title, "")
    head = Keyword.get(opts, :head, "")

    quote location: :keep do
      get("/", fn conn ->
        """
        <!DOCTYPE html>
        <html>
          <head>
            #{unquote(head)}

            <script src="https://unpkg.com/htmx.org/dist/htmx.js"></script>
            <title>#{unquote(title)}</title>
          </head>
          <body>
            #{unquote(content).(conn)}
          </body>
        </html>
        """
      end)
    end
  end

  @doc """
  Provides a sigil to render EEx content similar to ~H from Phoenix.LiveView

  Requires a variable named "assigns" to exist and be set to a map.
  """
  @spec sigil_E(String.t(), Keyword.t()) :: Macro.t()
  defmacro sigil_E(content, _opts \\ []) do
    unless Macro.Env.has_var?(__CALLER__, {:assigns, nil}) do
      raise "~E requires a variable named \"assigns\" to exist and be set to a map"
    end

    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
  end
end