lib/fermo/live/server.ex

defmodule Fermo.Live.Server do
  import Plug.Conn
  import Mix.Fermo.Paths, only: [app_path: 0]

  alias Fermo.Live.Dependencies

  require Logger

  live_reload_js_path = Application.app_dir(:fermo, "priv/static/fermo-live.js")
  @external_resource live_reload_js_path
  @live_reload_js """
  #{File.read!(live_reload_js_path)}
  #{Application.compile_env(:fermo, :live_reload_js, "")}
  """

  def init(_options) do
    []
  end

  def call(conn, _state) do
    conn |> request_path() |> handle_request_path(conn)
  end

  defp handle_request_path({:error, :request_path_missing}, conn) do
    respond_403(conn)
  end

  defp handle_request_path({:ok, request_path}, conn) do
    Logger.debug("GET #{request_path}")

    if is_static?(request_path) do
      serve_static(request_path, conn)
    else
      case find_page(request_path) do
        {:ok, page} ->
          Logger.debug("[Fermo.Live.Server] Serving page #{request_path}")
          serve_page(page, conn)

        _ ->
          Logger.debug("[Fermo.Live.Server] Page #{request_path} not found")
          respond_404(conn)
      end
    end
  end

  defp is_static?(path) do
    build_path = build_path(path)
    File.regular?(build_path)
  end

  defp serve_static(path, conn) do
    build_path = build_path(path)
    {:ok, extension} = extension(build_path)
    mime_type = mime_type(extension)
    respond_with_file(conn, build_path, mime_type)
  end

  defp find_page(request_path) do
    Fermo.Live.Dependencies.page_from_path(request_path)
  end

  defp serve_page(page, conn) do
    {:ok} = Fermo.Live.Dependencies.start_page(page.path)

    if page.params.layout do
      {:ok} = Fermo.Live.Dependencies.add_page_dependency(page.path, page.params.layout)
    end

    html = live_page(page)
    respond_with_html(conn, html)
  end

  defp live_page(page) do
    html = Fermo.Build.render_page(page)
    inject_reload(html)
  end

  defp inject_reload(html) do
    if has_body_close?(html) do
      [body | tail] = String.split(html, "</body>")
      Enum.join([body, live_reload_js() | tail], "\n")
    else
      Enum.join([html, live_reload_js()], "\n")
    end
  end

  defp has_body_close?(html) do
    String.contains?(html, "</body>")
  end

  defp respond_403(conn) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(403, "Forbidden")
  end

  defp respond_404(conn) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(404, "Not found")
  end

  defp respond_with_file(conn, full_path, mime_type) do
    conn
    |> put_resp_content_type(mime_type)
    |> send_resp(200, File.read!(full_path))
  end

  defp respond_with_html(conn, html) do
    conn
    |> put_resp_content_type("text/html")
    |> send_resp(200, html)
  end

  defp request_path(conn) do
    case conn.request_path do
      nil ->
        {:error, :request_path_missing}

      _ ->
        expanded = Path.expand(conn.request_path)

        if expanded == "/" do
          {:ok, "/"}
        else
          {:ok, URI.decode(expanded) <> "/"}
        end
    end
  end

  defp build_root do
    app_path()
    |> Path.join("build")
    |> Path.expand()
  end

  defp build_path(path) do
    Path.join(build_root(), path)
  end

  defp extension(path) do
    maybe_with_dot = Path.extname(path)

    cond do
      maybe_with_dot == "" ->
        {:ok, ""}

      maybe_with_dot == "." ->
        # We'll treat files with a final dot as HTML
        {:ok, ""}

      String.starts_with?(maybe_with_dot, ".") ->
        {:ok, String.slice(maybe_with_dot, 1..-1//1)}

      true ->
        {:error, :unexpected_extname_result}
    end
  end

  defp live_reload_js() do
    {:ok, generation} = Dependencies.generation()

    """
    <script type="text/javascript">
    window.fermoDependenciesGeneration = '#{generation}'

    #{@live_reload_js}
    </script>
    """
  end

  defp mime_type(extension) do
    case extension do
      "js" -> "application/javascript"
      "css" -> "text/css"
      "html" -> "text/html"
      "ico" -> "image/vnd.microsoft.icon"
      "jpg" -> "image/jpeg"
      "jpeg" -> "image/jpeg"
      "pdf" -> "application/pdf"
      "png" -> "image/png"
      "txt" -> "text/plain"
      "xml" -> "application/xml"
      _ -> "text/html"
    end
  end
end