/
template.ex
365 lines (291 loc) · 11.1 KB
/
template.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
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. We expose this private function
via `render/2`, which can be invoked as:
Templates.render("foo.html", %{name: "John Doe"})
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, :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, :format_encoders,
html: Phoenix.Template.HTML
"""
@type name :: binary
@type path :: binary
@type root :: binary
alias Phoenix.Template
@engines [eex: Phoenix.Template.EExEngine, exs: Phoenix.Template.ExsEngine]
@default_pattern "*"
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.relative_to_cwd exception.root}. "
<> 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__(options) do
quote bind_quoted: [options: options], unquote: true do
root = Keyword.fetch!(options, :root)
@phoenix_root Path.relative_to_cwd(root)
@phoenix_pattern Keyword.get(options, :pattern, unquote(@default_pattern))
@phoenix_template_engines Keyword.get(options, :template_engines, %{})
@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.
"""
@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
@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)
engines = Enum.into(engines, engines())
pairs = for path <- find_all(root, pattern) do
compile(path, root, engines)
end
names = Enum.map(pairs, &elem(&1, 0))
codes = Enum.map(pairs, &elem(&1, 1))
quote do
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 """
Returns the template root alongside all templates.
"""
def __templates__ do
{@phoenix_root, @phoenix_pattern, unquote(names)}
end
@doc """
Returns true whenever the list of templates changes in the filesystem.
"""
def __phoenix_recompile__? do
unquote(hash(root, pattern)) != Template.hash(@phoenix_root, @phoenix_pattern)
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, :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, :compiled_format_encoders, encoders)
encoders
end
end
defp default_encoders do
[html: Phoenix.Template.HTML, json: Phoenix.json_library(), js: Phoenix.Template.HTML]
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, :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, :compiled_template_engines, engines)
engines
end
end
defp raw_config(name) do
Application.get_env(:phoenix, name) ||
raise "could not load #{name} configuration for Phoenix. " <>
"Please ensure you have listed :phoenix under :applications in your " <>
"mix.exs file and have enabled the :phoenix compiler under :compilers, " <>
"for example: [:phoenix] ++ Mix.compilers"
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
|> Phoenix.Naming.unsuffix(suffix)
|> Module.split
|> Enum.drop(length(Module.split(base)))
|> Enum.map(&Phoenix.Naming.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) :: [path]
def find_all(root, pattern \\ @default_pattern) 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) :: binary
def hash(root, pattern \\ @default_pattern) do
find_all(root, pattern)
|> 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: assigns,
available: names,
template: template,
root: root,
pattern: pattern,
module: view_module
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, 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