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..b7ccceb 100644 --- a/lib/phoenix/template.ex +++ b/lib/phoenix/template.ex @@ -40,6 +40,32 @@ defmodule Phoenix.Template do config :phoenix_template, :format_encoders, html: Phoenix.HTML.Engine + ### HTML formats + + `.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 442023d..9eae2eb 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 + 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." + 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