defmodule Phoenix.Template do
@moduledoc """
Templates are used by Phoenix when rendering responses.
Since many views render significant content, for example a whole
HTML file, it is common to put these files into a particular directory,
typically "APP_web/templates".
This module provides conveniences for reading all files from a
particular directory and embedding them into a single module.
Imagine you have a directory with templates:
# templates/foo.html.eex
Hello <%= @name %>
# templates.ex
defmodule Templates do
use Phoenix.Template, root: "templates"
def render(template, assigns) do
render_template(template, assigns)
end
end
`Phoenix.Template` will define a private function named `render_template/2`
with one clause per file system template. You are responsible to expose
it appropriately, as shown above.
In practice, developers rarely use `Phoenix.Template` directly.
Instead they use `Phoenix.View` which wraps the template functionality
and adds some extra conveniences.
## Options
* `:root` - the root template path to find templates
* `:pattern` - the wildcard pattern to apply to the root
when finding templates. Default `"*"`
* `:template_engines` - a map of template engines extensions
to template engine handlers
## Terminology
Here is a quick introduction into Phoenix templates terms:
* template name - is the name of the template as
given by the user, without the template engine extension,
for example: "users.html"
* template path - is the complete path of the template
in the filesystem, for example, "path/to/users.html.eex"
* template root - the directory where templates are defined
* template engine - a module that receives a template path
and transforms its source code into Elixir quoted expressions
## Custom Template Engines
Phoenix supports custom template engines. Engines tell
Phoenix how to convert a template path into quoted expressions.
See `Phoenix.Template.Engine` for more information on
the API required to be implemented by custom engines.
Once a template engine is defined, you can tell Phoenix
about it via the template engines option:
config :phoenix_view, :template_engines,
eex: Phoenix.Template.EExEngine,
exs: Phoenix.Template.ExsEngine
If you want to support a given engine only on a certain template,
you can pass it as an option on `use Phoenix.Template`:
use Phoenix.Template, template_engines: %{
foo: Phoenix.Template.FooEngine
}
## Format encoders
Besides template engines, Phoenix has the concept of format encoders.
Format encoders work per format and are responsible for encoding a
given format to string once the view layer finishes processing.
A format encoder must export a function called `encode_to_iodata!/1`
which receives the rendering artifact and returns iodata.
New encoders can be added via the format encoder option:
config :phoenix_view, :format_encoders,
html: Phoenix.HTML.Engine
"""
@type name :: binary
@type path :: binary
@type root :: binary
alias Phoenix.Template
@engines [
eex: Phoenix.Template.EExEngine,
exs: Phoenix.Template.ExsEngine,
leex: Phoenix.LiveView.Engine,
heex: Phoenix.LiveView.HTMLEngine
]
@default_pattern "*"
@private_assigns [:__phx_template_not_found__]
defmodule UndefinedError do
@moduledoc """
Exception raised when a template cannot be found.
"""
defexception [:available, :template, :module, :root, :assigns, :pattern]
def message(exception) do
"Could not render #{inspect(exception.template)} for #{inspect(exception.module)}, " <>
"please define a matching clause for render/2 or define a template at " <>
"#{inspect(Path.join(Path.relative_to_cwd(exception.root), exception.pattern))}. " <>
available_templates(exception.available) <>
"\nAssigns:\n\n" <>
inspect(exception.assigns) <>
"\n\nAssigned keys: #{inspect(Map.keys(exception.assigns))}\n"
end
defp available_templates([]), do: "No templates were compiled for this module."
defp available_templates(available) do
"The following templates were compiled:\n\n" <>
Enum.map_join(available, "\n", &"* #{&1}") <>
"\n"
end
end
@doc false
defmacro __using__(opts) do
opts =
if Macro.quoted_literal?(opts) do
Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
else
opts
end
quote bind_quoted: [opts: opts], unquote: true do
Phoenix.Template.__options__(__MODULE__, opts)
@before_compile unquote(__MODULE__)
@doc """
Callback invoked when no template is found.
By default it raises but can be customized
to render a particular template.
"""
# Say it can return term in case the function is overridden.
@spec template_not_found(Phoenix.Template.name(), map) :: no_return
def template_not_found(template, assigns) do
Template.raise_template_not_found(__MODULE__, template, assigns)
end
defoverridable template_not_found: 2
end
end
defp expand_alias({:__aliases__, _, _} = alias, env),
do: Macro.expand(alias, %{env | function: {:init, 1}})
defp expand_alias(other, _env), do: other
@doc false
def __options__(module, options) do
root = Keyword.fetch!(options, :root)
Module.put_attribute(module, :phoenix_root, Path.relative_to_cwd(root))
Module.put_attribute(
module,
:phoenix_pattern,
Keyword.get(options, :pattern, unquote(@default_pattern))
)
Module.put_attribute(
module,
:phoenix_template_engines,
Enum.into(
Keyword.get(options, :template_engines, %{}),
Template.engines()
)
)
end
@doc false
defmacro __before_compile__(env) do
root = Module.get_attribute(env.module, :phoenix_root)
pattern = Module.get_attribute(env.module, :phoenix_pattern)
engines = Module.get_attribute(env.module, :phoenix_template_engines)
triplets =
for path <- find_all(root, pattern, engines) do
compile(path, root, engines)
end
names = Enum.map(triplets, &elem(&1, 0))
codes = Enum.map(triplets, &elem(&1, 2))
compile_time_deps =
for engine <- triplets |> Enum.map(&elem(&1, 1)) |> Enum.uniq() do
quote do
unquote(engine).__info__(:module)
end
end
quote do
unquote(compile_time_deps)
unquote(codes)
# Catch-all clause for template rendering.
defp render_template(template, %{__phx_render_existing__: {__MODULE__, template}}) do
nil
end
defp render_template(template, %{__phx_template_not_found__: __MODULE__} = assigns) do
Template.raise_template_not_found(__MODULE__, template, assigns)
end
defp render_template(template, assigns) do
template_not_found(template, Map.put(assigns, :__phx_template_not_found__, __MODULE__))
end
@doc false
def __templates__ do
{@phoenix_root, @phoenix_pattern, unquote(names)}
end
@doc false
def __mix_recompile__? do
unquote(hash(root, pattern, engines)) !=
Template.hash(@phoenix_root, @phoenix_pattern, @phoenix_template_engines)
end
end
end
@doc """
Returns the format encoder for the given template name.
"""
@spec format_encoder(name) :: module | nil
def format_encoder(template_name) when is_binary(template_name) do
Map.get(compiled_format_encoders(), Path.extname(template_name))
end
defp compiled_format_encoders do
case Application.fetch_env(:phoenix_view, :compiled_format_encoders) do
{:ok, encoders} ->
encoders
:error ->
encoders =
default_encoders()
|> Keyword.merge(raw_config(:format_encoders, []))
|> Enum.filter(fn {_, v} -> v end)
|> Enum.into(%{}, fn {k, v} -> {".#{k}", v} end)
Application.put_env(:phoenix_view, :compiled_format_encoders, encoders)
encoders
end
end
defp default_encoders do
[html: Phoenix.HTML.Engine, json: json_library(), js: Phoenix.HTML.Engine]
end
defp json_library() do
Application.get_env(:phoenix_view, :json_library) ||
Application.get_env(:phoenix, :json_library, Poison)
end
@doc """
Returns a keyword list with all template engines
extensions followed by their modules.
"""
@spec engines() :: %{atom => module}
def engines do
compiled_engines()
end
defp compiled_engines do
case Application.fetch_env(:phoenix_view, :compiled_template_engines) do
{:ok, engines} ->
engines
:error ->
engines =
@engines
|> Keyword.merge(raw_config(:template_engines, []))
|> Enum.filter(fn {_, v} -> v end)
|> Enum.into(%{})
Application.put_env(:phoenix_view, :compiled_template_engines, engines)
engines
end
end
defp raw_config(name, fallback) do
Application.get_env(:phoenix_view, name) || Application.get_env(:phoenix, name, fallback)
end
@doc """
Converts the template path into the template name.
## Examples
iex> Phoenix.Template.template_path_to_name(
...> "lib/templates/admin/users/show.html.eex",
...> "lib/templates")
"admin/users/show.html"
"""
@spec template_path_to_name(path, root) :: name
def template_path_to_name(path, root) do
path
|> Path.rootname()
|> Path.relative_to(root)
end
@doc """
Converts a module, without the suffix, to a template root.
## Examples
iex> Phoenix.Template.module_to_template_root(MyApp.UserView, MyApp, "View")
"user"
iex> Phoenix.Template.module_to_template_root(MyApp.Admin.User, MyApp, "View")
"admin/user"
iex> Phoenix.Template.module_to_template_root(MyApp.Admin.User, MyApp.Admin, "View")
"user"
iex> Phoenix.Template.module_to_template_root(MyApp.View, MyApp, "View")
""
iex> Phoenix.Template.module_to_template_root(MyApp.View, MyApp.View, "View")
""
"""
def module_to_template_root(module, base, suffix) do
module
|> unsuffix(suffix)
|> Module.split()
|> Enum.drop(length(Module.split(base)))
|> Enum.map(&Macro.underscore/1)
|> join_paths
end
defp join_paths([]), do: ""
defp join_paths(paths), do: Path.join(paths)
@doc """
Returns all template paths in a given template root.
"""
@spec find_all(root, pattern :: String.t(), %{atom => module}) :: [path]
def find_all(root, pattern \\ @default_pattern, engines \\ engines()) do
extensions = engines |> Map.keys() |> Enum.join(",")
root
|> Path.join(pattern <> ".{#{extensions}}")
|> Path.wildcard()
end
@doc """
Returns the hash of all template paths in the given root.
Used by Phoenix to check if a given root path requires recompilation.
"""
@spec hash(root, pattern :: String.t(), %{atom => module}) :: binary
def hash(root, pattern \\ @default_pattern, engines \\ engines()) do
find_all(root, pattern, engines)
|> Enum.sort()
|> :erlang.md5()
end
@doc false
def raise_template_not_found(view_module, template, assigns) do
{root, pattern, names} = view_module.__templates__()
raise UndefinedError,
assigns: Map.drop(assigns, @private_assigns),
available: names,
template: template,
root: root,
pattern: pattern,
module: view_module
end
@doc false
def resource_name(alias, suffix \\ "") do
alias
|> to_string()
|> Module.split()
|> List.last()
|> unsuffix(suffix)
|> Macro.underscore()
end
@doc false
defp unsuffix(value, suffix) do
string = to_string(value)
suffix_size = byte_size(suffix)
prefix_size = byte_size(string) - suffix_size
case string do
<<prefix::binary-size(prefix_size), ^suffix::binary>> -> prefix
_ -> string
end
end
defp compile(path, root, engines) do
name = template_path_to_name(path, root)
defp = String.to_atom(name)
ext = Path.extname(path) |> String.trim_leading(".") |> String.to_atom()
engine = Map.fetch!(engines, ext)
quoted = engine.compile(path, name)
{name, engine,
quote do
@file unquote(path)
@external_resource unquote(path)
defp unquote(defp)(var!(assigns)) do
_ = var!(assigns)
unquote(quoted)
end
defp render_template(unquote(name), assigns) do
unquote(defp)(assigns)
end
end}
end
end