This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

Binary file not shown.

View File

@@ -0,0 +1,21 @@
# CHANGELOG
## 1.0.4
* Relax Phoenix.HTML dependency
## 1.0.3
* Improve error message for invalid file paths
## 1.0.2
* Improve error message for missing templates
## 1.0.1
* Add `embed_templates/2` for convenience
## 1.0.0
* Initial release

View File

@@ -0,0 +1,22 @@
# MIT License
Copyright (c) 2014 Chris McCord
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
# Phoenix.Template
Templates are markup languages that are compiled to Elixir code.
This packages provides functions for loading and compiling templates
from disk as well as the behaviour for compiling markup languages.
See the [`Phoenix.Template`](https://hexdocs.pm/phoenix_template/Phoenix.Template.html)
module documentation for more information.
## Installation
You can install `phoenix_template` by adding it to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:phoenix_template, "~> 1.0"}
]
end
```

View File

@@ -0,0 +1,22 @@
{<<"links">>,
[{<<"GitHub">>,<<"https://github.com/phoenixframework/phoenix_template">>}]}.
{<<"name">>,<<"phoenix_template">>}.
{<<"version">>,<<"1.0.4">>}.
{<<"description">>,<<"Template rendering for Phoenix">>}.
{<<"elixir">>,<<"~> 1.9">>}.
{<<"app">>,<<"phoenix_template">>}.
{<<"licenses">>,[<<"MIT">>]}.
{<<"requirements">>,
[[{<<"name">>,<<"phoenix_html">>},
{<<"app">>,<<"phoenix_html">>},
{<<"optional">>,true},
{<<"requirement">>,<<"~> 2.14.2 or ~> 3.0 or ~> 4.0">>},
{<<"repository">>,<<"hexpm">>}]]}.
{<<"files">>,
[<<"lib">>,<<"lib/phoenix">>,<<"lib/phoenix/template">>,
<<"lib/phoenix/template/exs_engine.ex">>,
<<"lib/phoenix/template/eex_engine.ex">>,
<<"lib/phoenix/template/engine.ex">>,<<"lib/phoenix/template.ex">>,
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>,
<<"CHANGELOG.md">>]}.
{<<"build_tools">>,[<<"mix">>]}.

View File

@@ -0,0 +1,509 @@
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 """
Ensure `__mix_recompile__?/0` will be defined.
"""
defmacro __using__(_opts) do
quote do
Phoenix.Template.__idempotent_setup__(__MODULE__, %{})
end
end
@doc """
A convenience macro for embeding templates as functions.
This macro is built on top of the more general `compile_all/3`
functionality.
## Options
* `:root` - The root directory to embed files. Defaults to the current
module's directory (`__DIR__`)
* `:suffix` - The string value to append to embedded function names. By
default, function names will be the name of the template file excluding
the format and engine.
A wildcard pattern may be used to select all files within a directory tree.
For example, imagine a directory listing:
├── pages
│ ├── about.html.heex
│ └── sitemap.xml.eex
Then to embed the templates in your module:
defmodule MyAppWeb.Renderer do
import Phoenix.Template, only: [embed_templates: 1]
embed_templates "pages/*"
end
Now, your module will have a `about/1` and `sitemap/1` functions.
Note that functions across different formats were embedded. In case
you want to distinguish between them, you can give a more specific
pattern:
defmodule MyAppWeb.Emails do
import Phoenix.Template, only: [embed_templates: 2]
embed_templates "pages/*.html", suffix: "_html"
embed_templates "pages/*.xml", suffix: "_xml"
end
Now the functions will be `about_html` and `sitemap_xml`.
"""
@doc type: :macro
defmacro embed_templates(pattern, opts \\ []) do
quote bind_quoted: [pattern: pattern, opts: opts] do
Phoenix.Template.compile_all(
&Phoenix.Template.__embed__(&1, opts[:suffix]),
Path.expand(opts[:root] || __DIR__, __DIR__),
pattern
)
end
end
@doc false
def __embed__(path, suffix),
do:
path
|> Path.basename()
|> Path.rootname()
|> Path.rootname()
|> Kernel.<>(suffix || "")
@doc """
Renders the template and returns iodata.
"""
def render_to_iodata(module, template, format, assign) do
module
|> render(template, format, assign)
|> encode(format)
end
@doc """
Renders the template to string.
"""
def render_to_string(module, template, format, assign) do
module
|> render_to_iodata(template, format, assign)
|> IO.iodata_to_binary()
end
@doc """
Renders template from module.
For a module called `MyApp.FooHTML` and template "index.html.heex",
it will:
* First attempt to call `MyApp.FooHTML.index(assigns)`
* Then fallback to `MyApp.FooHTML.render("index.html", assigns)`
* Raise otherwise
It expects the HTML module, the template as a string, the format, and a
set of assigns.
Notice that this function returns the inner representation of a
template. If you want the encoded template as a result, use
`render_to_iodata/4` instead.
## Examples
Phoenix.Template.render(YourApp.UserView, "index", "html", name: "John Doe")
#=> {:safe, "Hello John Doe"}
## Assigns
Assigns are meant to be user data that will be available in templates.
However, there are keys under assigns that are specially handled by
Phoenix, they are:
* `:layout` - tells Phoenix to wrap the rendered result in the
given layout. See next section
## Layouts
Templates can be rendered within other templates using the `:layout`
option. `:layout` accepts a tuple of the form
`{LayoutModule, "template.extension"}`.
To template that goes inside the layout will be placed in the `@inner_content`
assign:
<%= @inner_content %>
"""
def render(module, template, format, assigns) do
assigns
|> Map.new()
|> Map.pop(:layout, false)
|> render_within_layout(module, template, format)
end
defp render_within_layout({false, assigns}, module, template, format) do
render_with_fallback(module, template, format, assigns)
end
defp render_within_layout({{layout_mod, layout_tpl}, assigns}, module, template, format)
when is_atom(layout_mod) and is_binary(layout_tpl) do
content = render_with_fallback(module, template, format, assigns)
assigns = Map.put(assigns, :inner_content, content)
render_with_fallback(layout_mod, layout_tpl, format, assigns)
end
defp render_within_layout({layout, _assigns}, _module, _template, _format) do
raise ArgumentError, """
invalid value for reserved key :layout in Phoenix.Template.render/4 assigns.
:layout accepts a tuple of the form {LayoutModule, "template.extension"},
got: #{inspect(layout)}
"""
end
defp encode(content, format) do
if encoder = format_encoder(format) do
encoder.encode_to_iodata!(content)
else
content
end
end
defp render_with_fallback(module, template, format, assigns)
when is_atom(module) and is_binary(template) and is_binary(format) and is_map(assigns) do
:erlang.module_loaded(module) or :code.ensure_loaded(module)
try do
String.to_existing_atom(template)
catch
_, _ -> fallback_render(module, template, format, assigns)
else
atom ->
if function_exported?(module, atom, 1) do
apply(module, atom, [assigns])
else
fallback_render(module, template, format, assigns)
end
end
end
@compile {:inline, fallback_render: 4}
defp fallback_render(module, template, format, assigns) do
if function_exported?(module, :render, 2) do
module.render(template <> "." <> format, assigns)
else
reason =
if Code.ensure_loaded?(module) do
" (the module exists but does not define #{template}/1 nor render/2)"
else
" (the module does not exist)"
end
raise ArgumentError,
"no \"#{template}\" #{format} template defined for #{inspect(module)} #{reason}"
end
end
## Configuration API
@doc """
Returns the format encoder for the given template.
"""
@spec format_encoder(format :: String.t()) :: module | nil
def format_encoder(format) when is_binary(format) do
Map.get(compiled_format_encoders(), format)
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} -> {to_string(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
@doc """
Compiles a function for each template in the given `root`.
`converter` is an anonymous function that receives the template path
and returns the function name (as a string).
For example, to compile all `.eex` templates in a given directory,
you might do:
Phoenix.Template.compile_all(
&(&1 |> Path.basename() |> Path.rootname(".eex")),
__DIR__,
"*.eex"
)
If the directory has templates named `foo.eex` and `bar.eex`,
they will be compiled into the functions `foo/1` and `bar/1`
that receive the template `assigns` as argument.
You may optionally pass a keyword list of engines. If a list
is given, we will lookup and compile only this subset of engines.
If none is passed (`nil`), the default list returned by `engines/0`
is used.
"""
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::binary-size(prefix_size), ^suffix::binary>> -> prefix
_ -> string
end
end
end

View File

@@ -0,0 +1,56 @@
defmodule Phoenix.Template.EExEngine do
@moduledoc """
The Phoenix engine that handles the `.eex` extension.
"""
@behaviour Phoenix.Template.Engine
def compile(path, _name) do
EEx.compile_file(path, [line: 1] ++ options_for(path))
end
defp options_for(path) do
format =
case path |> Path.rootname() |> Path.extname() do
"." <> format ->
format
_ ->
raise ArgumentError,
"template paths in Phoenix require the format extension, got: #{path}"
end
case Phoenix.Template.format_encoder(format) do
Phoenix.HTML.Engine ->
unless Code.ensure_loaded?(Phoenix.HTML.Engine) do
raise "could not load Phoenix.HTML.Engine to use with .html.eex templates. " <>
"You can configure your own format encoder for HTML but we recommend " <>
"adding phoenix_html as a dependency as it provides XSS protection."
end
trim =
case Application.get_env(:phoenix_template, :trim_on_html_eex_engine) do
nil ->
case Application.get_env(:phoenix_view, :trim_on_html_eex_engine) do
nil ->
Application.get_env(:phoenix, :trim_on_html_eex_engine, true)
boolean ->
IO.warn(
"config :phoenix_view, :trim_on_html_eex_engine is deprecated, please use config :phoenix_template, :trim_on_html_eex_engine instead"
)
boolean
end
boolean ->
boolean
end
[engine: Phoenix.HTML.Engine, trim: trim]
_ ->
[engine: EEx.SmartEngine]
end
end
end

View File

@@ -0,0 +1,15 @@
defmodule Phoenix.Template.Engine do
@moduledoc """
Specifies the API for adding custom template engines into Phoenix.
Engines must implement the `compile/2` function, that receives
the template file and the template name (usually used as the function
name of the template) and outputs the template quoted expression:
def compile(template_path, template_name)
See `Phoenix.Template.EExEngine` for an example engine implementation.
"""
@callback compile(template_path :: binary, template_name :: binary) :: Macro.t()
end

View File

@@ -0,0 +1,13 @@
defmodule Phoenix.Template.ExsEngine do
@moduledoc """
The Phoenix engine that handles the `.exs` extension.
"""
@behaviour Phoenix.Template.Engine
def compile(path, _name) do
path
|> File.read!()
|> Code.string_to_quoted!(file: path)
end
end

View File

@@ -0,0 +1,49 @@
defmodule PhoenixTemplate.MixProject do
use Mix.Project
@version "1.0.4"
@source_url "https://github.com/phoenixframework/phoenix_template"
def project do
[
app: :phoenix_template,
version: @version,
elixir: "~> 1.9",
description: "Template rendering for Phoenix",
docs: docs(),
deps: deps(),
package: package()
]
end
def application do
[
extra_applications: [:eex]
]
end
defp docs do
[
main: "Phoenix.Template",
source_url: @source_url,
source_ref: "v#{@version}",
extras: ["CHANGELOG.md"]
]
end
defp deps do
[
{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", optional: true},
{:jason, "~> 1.0", only: :test},
{:ex_doc, "~> 0.22", only: :docs}
]
end
defp package do
[
maintainers: ["Chris McCord", "José Valim", "Gary Rennie"],
licenses: ["MIT"],
links: %{"GitHub" => @source_url}
]
end
end