defmodule Phoenix.Template do @moduledoc """ Templates are markup languages that are compiled to Elixir code. This module provides functions for loading and compiling templates from disk. A markup language is compiled to Elixir code via an engine. See `Phoenix.Template.Engine`. In practice, developers rarely use `Phoenix.Template` directly. Instead, libraries such as `Phoenix.View` and `Phoenix.LiveView` use it as a building block. ## 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, :template_engines, eex: Phoenix.Template.EExEngine, exs: Phoenix.Template.ExsEngine ## 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 a string. For example, when rendering JSON, your templates may return a regular Elixir map. Then the JSON format encoder is invoked to convert it to JSON. 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_template, :format_encoders, html: Phoenix.HTML.Engine """ @type path :: binary @type root :: binary @default_pattern "*" @doc false defmacro __using__(_opts) do quote do Phoenix.Template.__idempotent_setup__(__MODULE__, %{}) end end ## Configuration API @doc """ Returns the format encoder for the given template. """ @spec format_encoder(path) :: module | nil def format_encoder(path) when is_binary(path) do Map.get(compiled_format_encoders(), Path.extname(path)) end defp compiled_format_encoders do case Application.fetch_env(:phoenix_template, :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_template, :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_template, :json_library) || deprecated_config(:phoenix_view, :json_library) || Application.get_env(:phoenix, :json_library, Jason) 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_template, :compiled_template_engines) do {:ok, engines} -> engines :error -> engines = default_engines() |> Keyword.merge(raw_config(:template_engines, [])) |> Enum.filter(fn {_, v} -> v end) |> Enum.into(%{}) Application.put_env(:phoenix_template, :compiled_template_engines, engines) engines end end defp default_engines do [ eex: Phoenix.Template.EExEngine, exs: Phoenix.Template.ExsEngine, leex: Phoenix.LiveView.Engine, heex: Phoenix.LiveView.HTMLEngine ] end defp raw_config(name, fallback) do Application.get_env(:phoenix_template, name) || deprecated_config(:phoenix_view, name) || Application.get_env(:phoenix, name, fallback) end defp deprecated_config(app, name) do if value = Application.get_env(app, name) do IO.warn( "config :#{app}, :#{name} is deprecated, please use config :phoenix_template, :#{name} instead" ) value end end ## Lookup API @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 defmacro compile_all(converter, root, pattern \\ @default_pattern, engines \\ nil) do quote bind_quoted: binding() do for {path, name, body} <- Phoenix.Template.__compile_all__(__MODULE__, converter, root, pattern, engines) do @external_resource path @file path def unquote(String.to_atom(name))(var!(assigns)) do _ = var!(assigns) unquote(body) end {name, path} end end end @doc false def __compile_all__(module, converter, root, pattern, given_engines) do engines = given_engines || engines() paths = find_all(root, pattern, engines) {triplets, {paths, engines}} = Enum.map_reduce(paths, {[], %{}}, fn path, {acc_paths, acc_engines} -> ext = Path.extname(path) |> String.trim_leading(".") |> String.to_atom() engine = Map.fetch!(engines, ext) name = converter.(path) body = engine.compile(path, name) map = {path, name, body} reduce = {[path | acc_paths], Map.put(acc_engines, engine, true)} {map, reduce} end) # Store the engines so we define compile-time deps __idempotent_setup__(module, engines) # Store the hashes so we define __mix_recompile__? hash = paths |> Enum.sort() |> :erlang.md5() args = if given_engines, do: [root, pattern, Macro.escape(given_engines)], else: [root, pattern] Module.put_attribute(module, :phoenix_templates_hashes, {hash, args}) triplets end @doc false def __idempotent_setup__(module, engines) do # Store the used engines so they become requires on before_compile if used_engines = Module.get_attribute(module, :phoenix_templates_engines) do Module.put_attribute(module, :phoenix_templates_engines, Map.merge(used_engines, engines)) else Module.register_attribute(module, :phoenix_templates_hashes, accumulate: true) Module.put_attribute(module, :phoenix_templates_engines, engines) Module.put_attribute(module, :before_compile, Phoenix.Template) end end @doc false defmacro __before_compile__(env) do hashes = Module.get_attribute(env.module, :phoenix_templates_hashes) engines = Module.get_attribute(env.module, :phoenix_templates_engines) body = Enum.reduce(hashes, false, fn {hash, args}, acc -> quote do unquote(acc) or unquote(hash) != Phoenix.Template.hash(unquote_splicing(args)) end end) compile_time_deps = for {engine, _} <- engines do quote do unquote(engine).__info__(:module) end end quote do unquote(compile_time_deps) @doc false def __mix_recompile__? do unquote(body) end end end ## Deprecated API @deprecated "Use Phoenix.View.template_path_to_name/3" def template_path_to_name(path, root) do path |> Path.rootname() |> Path.relative_to(root) end @deprecated "Use Phoenix.View.module_to_template_root/3" 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) 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 end