defmodule ElixirLatex.Job do
defmodule LatexError do
defexception message: "LaTeX compilation job failed with an error"
@moduledoc """
Error raised when a LaTeX compilation job exits with a non-zero code.
"""
end
@type assigns :: %{optional(atom) => any}
@type attachments :: %{optional(atom | binary) => iodata}
@type layout :: {atom, binary | atom} | false
@type view :: atom | false
@type renderer :: binary | :xelatex | :latex | :pdflatex
@type body :: iodata | nil
@type t :: %__MODULE__{
assigns: assigns,
attachments: attachments,
layout: layout,
view: view,
job_name: binary | nil,
renderer: renderer,
body: body
}
defstruct assigns: %{},
attachments: %{},
layout: false,
view: false,
job_name: nil,
renderer: :xelatex,
body: nil
alias ElixirLatex.Job
alias ElixirLatex.Attachment
@spec assign(t, atom, term) :: t
def assign(%Job{assigns: assigns} = job, key, value) when is_atom(key) do
%{job | assigns: Map.put(assigns, key, value)}
end
@spec put_attachment(t, atom | binary, iodata) :: t
def put_attachment(%Job{attachments: attachments} = job, key, value)
when is_atom(key) do
%{job | attachments: Map.put(attachments, key, value)}
end
@spec put_data_url_attachment(t, atom | binary, binary) :: t | :error
def put_data_url_attachment(%Job{attachments: attachments} = job, key, data_url) do
with %Attachment{} = attachment <- Attachment.from_data_url(data_url) do
%{job | attachments: Map.put(attachments, key, attachment)}
end
end
@spec put_layout(t, layout) :: t
def put_layout(%Job{} = job, layout) do
%{job | layout: layout}
end
@spec put_view(t, view) :: t
def put_view(%Job{} = job, view) do
%{job | view: view}
end
@spec set_renderer(t, renderer) :: t
def set_renderer(%Job{} = job, renderer) when is_atom(renderer) or is_binary(renderer) do
%{job | renderer: renderer}
end
@spec put_body(t, body) :: t
def put_body(%Job{} = job, body) do
%{job | body: body}
end
@spec render(t, binary) :: {:ok, binary} | {:error, term}
def render(job, template, assigns \\ [])
def render(%Job{} = job, template, assigns) when is_binary(template) do
job = maybe_set_job_name(job) |> assign_attachments()
assigns = merge_assigns(job.assigns, assigns)
source = render_with_layout(job, template, assigns)
job = put_body(job, source)
ElixirLatex.Renderer.render_to_pdf(job)
end
defp merge_assigns(original, overrides) do
Map.merge(to_map(original), to_map(overrides))
end
defp to_map(assigns) when is_map(assigns), do: assigns
defp to_map(assigns) when is_list(assigns), do: :maps.from_list(assigns)
defp random_job_name do
:crypto.strong_rand_bytes(10)
|> Base.encode16(case: :lower)
end
defp maybe_set_job_name(%Job{job_name: nil} = job) do
%{job | job_name: random_job_name()}
end
defp maybe_set_job_name(job), do: job
defp assign_attachments(%Job{attachments: attachments, assigns: assigns} = job) do
attachments =
for {key, %{filename: filename, extension: extension}} <- attachments, into: %{} do
{key, "#{filename}.#{extension}"}
end
%{job | assigns: Map.put(assigns, :attachments, attachments)}
end
defp render_with_layout(job, template, assigns) do
render_assigns = Map.put(assigns, :job, job)
case job.layout do
{layout_mod, layout_tpl} ->
inner = Phoenix.View.render(job.view, template, render_assigns)
root_assigns = render_assigns |> Map.put(:inner_content, inner) |> Map.delete(:layout)
Phoenix.View.render_to_iodata(layout_mod, "#{layout_tpl}.tex", root_assigns)
false ->
Phoenix.View.render_to_iodata(job.view, template, render_assigns)
end
end
end