From a057a924bff921bebee0631e2e666ee9675c4b5a Mon Sep 17 00:00:00 2001 From: Tim Masliuchenko Date: Mon, 21 Sep 2026 16:08:40 +0100 Subject: [PATCH 1/2] Add `:html_formats` option --- config/config.exs | 6 +++ lib/phoenix/template.ex | 25 ++++++++++ lib/phoenix/template/eex_engine.ex | 61 ++++++++++++++----------- test/fixtures/templates/show.custom.eex | 1 + test/fixtures/templates/show.markup.eex | 1 + test/fixtures/templates/trim.markup.eex | 3 ++ test/phoenix/template_test.exs | 34 ++++++++++++++ 7 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 test/fixtures/templates/show.custom.eex create mode 100644 test/fixtures/templates/show.markup.eex create mode 100644 test/fixtures/templates/trim.markup.eex diff --git a/config/config.exs b/config/config.exs index 3c42a33..fbaa96d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,3 +3,9 @@ import Config config :logger, :console, colors: [enabled: false] config :phoenix_template, :json_library, Jason config :phoenix_template, :trim_on_html_eex_engine, true + +config :phoenix_template, :format_encoders, + markup: Phoenix.TemplateTest.MarkupEncoder, + custom: Phoenix.TemplateTest.CustomEncoder + +config :phoenix_template, :html_formats, [:markup] diff --git a/lib/phoenix/template.ex b/lib/phoenix/template.ex index a452d86..838efe4 100644 --- a/lib/phoenix/template.ex +++ b/lib/phoenix/template.ex @@ -40,6 +40,31 @@ defmodule Phoenix.Template do config :phoenix_template, :format_encoders, html: Phoenix.HTML.Engine + ### HTML formats + + `.eex` templates are compiled with `Phoenix.HTML.Engine` only when the + encoder of their format is `Phoenix.HTML.Engine` itself. Templates of any + other format are compiled with `EEx.SmartEngine`, which interpolates + values as is. + + A format that renders HTML-like markup but needs its own encoder, for + example one that post-processes the rendered markup, can be compiled + with `Phoenix.HTML.Engine` too by listing it in the `:html_formats` option: + + config :phoenix_template, :format_encoders, mjml: MyApp.MJML + config :phoenix_template, :html_formats, [:mjml] + + Its templates are then compiled exactly like `.html.eex` ones. Interpolated + values are escaped unless they are marked as safe, such as with + `Phoenix.HTML.raw/1`, the `:trim_on_html_eex_engine` option applies, and + the format encoder receives the `{:safe, iodata}` tuple that they return: + + defmodule MyApp.MJML do + def encode_to_iodata!({:safe, mjml}), do: MyApp.MJML.to_html(mjml) + end + + This requires `phoenix_html` as a dependency. + """ @type path :: binary diff --git a/lib/phoenix/template/eex_engine.ex b/lib/phoenix/template/eex_engine.ex index 442023d..c6c3432 100644 --- a/lib/phoenix/template/eex_engine.ex +++ b/lib/phoenix/template/eex_engine.ex @@ -20,37 +20,46 @@ defmodule Phoenix.Template.EExEngine do "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) + if html_format?(format) do + unless Code.ensure_loaded?(Phoenix.HTML.Engine) do + raise "could not load Phoenix.HTML.Engine to use with .#{format}.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 - boolean -> - IO.warn( - "config :phoenix_view, :trim_on_html_eex_engine is deprecated, please use config :phoenix_template, :trim_on_html_eex_engine instead" - ) + 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 - end + 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 -> - boolean - end + boolean + end - [engine: Phoenix.HTML.Engine, trim: trim] + boolean -> + boolean + end - _ -> - [engine: EEx.SmartEngine] + [engine: Phoenix.HTML.Engine, trim: trim] + else + [engine: EEx.SmartEngine] end end + + defp html_format?(format) do + Phoenix.Template.format_encoder(format) == Phoenix.HTML.Engine or + format in html_formats() + end + + defp html_formats do + :phoenix_template + |> Application.get_env(:html_formats, []) + |> Enum.map(&to_string/1) + end end diff --git a/test/fixtures/templates/show.custom.eex b/test/fixtures/templates/show.custom.eex new file mode 100644 index 0000000..dfc0b3a --- /dev/null +++ b/test/fixtures/templates/show.custom.eex @@ -0,0 +1 @@ +
Custom! <%= @message %>
diff --git a/test/fixtures/templates/show.markup.eex b/test/fixtures/templates/show.markup.eex new file mode 100644 index 0000000..fba3ced --- /dev/null +++ b/test/fixtures/templates/show.markup.eex @@ -0,0 +1 @@ +
Markup! <%= @message %>
diff --git a/test/fixtures/templates/trim.markup.eex b/test/fixtures/templates/trim.markup.eex new file mode 100644 index 0000000..9d48221 --- /dev/null +++ b/test/fixtures/templates/trim.markup.eex @@ -0,0 +1,3 @@ +<%= 12 %> + <%= 34 %> +<%= 56 %> diff --git a/test/phoenix/template_test.exs b/test/phoenix/template_test.exs index 3b79052..c30eeb4 100644 --- a/test/phoenix/template_test.exs +++ b/test/phoenix/template_test.exs @@ -1,3 +1,11 @@ +defmodule Phoenix.TemplateTest.MarkupEncoder do + def encode_to_iodata!({:safe, body}), do: ["markup:", body] +end + +defmodule Phoenix.TemplateTest.CustomEncoder do + def encode_to_iodata!(body), do: ["custom:", body] +end + defmodule Phoenix.TemplateTest do use ExUnit.Case, async: true @@ -74,10 +82,36 @@ defmodule Phoenix.TemplateTest do refute AllTemplates.__mix_recompile__?() end + test "compiles templates of html formats with Phoenix.HTML.Engine" do + assert AllTemplates.show_markup_eex(%{message: ""}) + |> Phoenix.HTML.safe_to_string() == + "
Markup! <hello>
\n" + + assert AllTemplates.show_markup_eex(%{message: {:safe, ""}}) + |> Phoenix.HTML.safe_to_string() == + "
Markup!
\n" + + assigns = %{message: ""} + + assert Template.render_to_string(AllTemplates, "show_markup_eex", "markup", assigns) == + "markup:
Markup! <hello>
\n" + end + + test "compiles templates of other formats with EEx.SmartEngine" do + assert AllTemplates.show_custom_eex(%{message: ""}) == + "
Custom!
\n" + + assigns = %{message: ""} + + assert Template.render_to_string(AllTemplates, "show_custom_eex", "custom", assigns) == + "custom:
Custom!
\n" + end + if Version.match?(System.version(), ">= 1.12.0") do test "trims only compiled HTML files" do assert AllTemplates.no_trim_text_eex(%{}) == "12\n 34\n56\n" assert AllTemplates.trim_html_eex(%{}) |> Phoenix.HTML.safe_to_string() == "12\n34\n56" + assert AllTemplates.trim_markup_eex(%{}) |> Phoenix.HTML.safe_to_string() == "12\n34\n56" end end From b4b7768146684e4d2e0c562e28f1996da9bb079f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 21 Sep 2026 18:35:17 +0200 Subject: [PATCH 2/2] Apply batched suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/phoenix/template.ex | 47 +++++++++++++++--------------- lib/phoenix/template/eex_engine.ex | 2 +- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/lib/phoenix/template.ex b/lib/phoenix/template.ex index 838efe4..b7ccceb 100644 --- a/lib/phoenix/template.ex +++ b/lib/phoenix/template.ex @@ -42,29 +42,30 @@ defmodule Phoenix.Template do ### HTML formats - `.eex` templates are compiled with `Phoenix.HTML.Engine` only when the - encoder of their format is `Phoenix.HTML.Engine` itself. Templates of any - other format are compiled with `EEx.SmartEngine`, which interpolates - values as is. - - A format that renders HTML-like markup but needs its own encoder, for - example one that post-processes the rendered markup, can be compiled - with `Phoenix.HTML.Engine` too by listing it in the `:html_formats` option: - - config :phoenix_template, :format_encoders, mjml: MyApp.MJML - config :phoenix_template, :html_formats, [:mjml] - - Its templates are then compiled exactly like `.html.eex` ones. Interpolated - values are escaped unless they are marked as safe, such as with - `Phoenix.HTML.raw/1`, the `:trim_on_html_eex_engine` option applies, and - the format encoder receives the `{:safe, iodata}` tuple that they return: - - defmodule MyApp.MJML do - def encode_to_iodata!({:safe, mjml}), do: MyApp.MJML.to_html(mjml) - end - - This requires `phoenix_html` as a dependency. - + `.html.eex` templates are compiled and encoded with `Phoenix.HTML.Engine`, + which provides HTML safety. Other formats are compiled with `EEx.SmartEngine`, + which interpolates values as is. + + A format that renders HTML-like markup but needs its own encoder, for + example one that post-processes the rendered markup, can be compiled + with `Phoenix.HTML.Engine` too by listing it in the `:html_formats` option: + + config :phoenix_template, :format_encoders, mjml: MyApp.MJML + config :phoenix_template, :html_formats, [:html, :mjml] + + Its templates are then compiled exactly like `.html.eex` ones. Interpolated + values are escaped unless they are marked as safe, such as with + `Phoenix.HTML.raw/1`, the `:trim_on_html_eex_engine` option applies, and + the format encoder receives the `{:safe, iodata}` tuple that they return: + + defmodule MyApp.MJML do + def encode_to_iodata!({:safe, mjml}), do: MyApp.MJML.to_html(mjml) + end + + In case the `{:safe, ...}` tuple is not received, you should raise, as it means + the user likely has not configured the `html_formats` accordingly. + + This requires `phoenix_html` as a dependency. """ @type path :: binary diff --git a/lib/phoenix/template/eex_engine.ex b/lib/phoenix/template/eex_engine.ex index c6c3432..9eae2eb 100644 --- a/lib/phoenix/template/eex_engine.ex +++ b/lib/phoenix/template/eex_engine.ex @@ -21,7 +21,7 @@ defmodule Phoenix.Template.EExEngine do end if html_format?(format) do - unless Code.ensure_loaded?(Phoenix.HTML.Engine) do + if not Code.ensure_loaded?(Phoenix.HTML.Engine) do raise "could not load Phoenix.HTML.Engine to use with .#{format}.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."