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. """ # Do not add a spec as they are not overridable and # the user may want to return an actual value # @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 _ -> 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