diff --git a/lib/phoenix_html/tag.ex b/lib/phoenix_html/tag.ex index 3e3ab73..447538b 100644 --- a/lib/phoenix_html/tag.ex +++ b/lib/phoenix_html/tag.ex @@ -8,7 +8,8 @@ defmodule Phoenix.HTML.Tag do import Phoenix.HTML - @tag_prefixes [:aria, :data] + @special_attributes ["data", "aria", "class"] + @csrf_param "_csrf_token" @method_param "_method" @@ -49,7 +50,7 @@ defmodule Phoenix.HTML.Tag do def tag(name), do: tag(name, []) def tag(name, attrs) when is_list(attrs) do - {:safe, [?<, to_string(name), build_attrs(attrs), ?>]} + {:safe, [?<, to_string(name), build_attrs(attrs) |> Enum.sort() |> tag_attrs(), ?>]} end @doc ~S""" @@ -87,16 +88,49 @@ defmodule Phoenix.HTML.Tag do def content_tag(name, content, attrs) when is_list(attrs) do name = to_string(name) {:safe, escaped} = html_escape(content) - {:safe, [?<, name, build_attrs(attrs), ?>, escaped, ?<, ?/, name, ?>]} + + {:safe, + [?<, name, build_attrs(attrs) |> Enum.sort() |> tag_attrs(), ?>, escaped, ?<, ?/, name, ?>]} + end + + @doc """ + Escapes a list of attributes, returning iodata. + + Pay attention that, unlike `tag/2` and `content_tag/2`, this + function does not sort the attributes. + + iex> attributes_escape(title: "the title", id: "the id", selected: true) + {:safe, + [ + [32, "title", 61, 34, "the title", 34], + [32, "id", 61, 34, "the id", 34], + [32, "selected"] + ]} + + """ + def attributes_escape(attrs) do + {:safe, attrs |> build_attrs() |> Enum.reverse() |> tag_attrs()} end defp build_attrs([]), do: [] defp build_attrs(attrs), do: build_attrs(attrs, []) - defp build_attrs([], acc), do: acc |> Enum.sort() |> tag_attrs() + defp build_attrs([], acc), do: acc - defp build_attrs([{k, v} | t], acc) when k in @tag_prefixes and is_list(v) do - build_attrs(t, nested_attrs(key_escape(k), v, acc)) + defp build_attrs([{k, v} | t], acc) when k in @special_attributes do + build_attrs([{String.to_atom(k), v} | t], acc) + end + + defp build_attrs([{:data, v} | t], acc) when is_list(v) do + build_attrs(t, nested_attrs("data", v, acc)) + end + + defp build_attrs([{:aria, v} | t], acc) when is_list(v) do + build_attrs(t, nested_attrs("aria", v, acc)) + end + + defp build_attrs([{:class, v} | t], acc) when is_list(v) do + build_attrs(t, [{"class", class_value(v)} | acc]) end defp build_attrs([{k, true} | t], acc) do @@ -137,6 +171,16 @@ defmodule Phoenix.HTML.Tag do end) end + defp class_value(value) when is_list(value) do + value + |> Enum.filter(& &1) + |> Enum.join(" ") + end + + defp class_value(value) do + value + end + defp key_escape(value) when is_atom(value), do: String.replace(Atom.to_string(value), "_", "-") defp key_escape(value), do: attr_escape(value) diff --git a/test/phoenix_html/tag_test.exs b/test/phoenix_html/tag_test.exs index b867b01..bdbd624 100644 --- a/test/phoenix_html/tag_test.exs +++ b/test/phoenix_html/tag_test.exs @@ -5,6 +5,61 @@ defmodule Phoenix.HTML.TagTest do import Phoenix.HTML.Tag doctest Phoenix.HTML.Tag + describe "attributes_escape" do + test "key as atom" do + assert attributes_escape([{:title, "the title"}]) |> safe_to_string() == + ~s( title="the title") + end + + test "key as string" do + assert attributes_escape([{"title", "the title"}]) |> safe_to_string() == + ~s( title="the title") + end + + test "convert snake_case keys into kebab-case when key is atom" do + assert attributes_escape([{:my_attr, "value"}]) |> safe_to_string() == ~s( my-attr="value") + end + + test "keep snake_case keys when key is string" do + assert attributes_escape([{"my_attr", "value"}]) |> safe_to_string() == ~s( my_attr="value") + end + + test "multiple attributes" do + assert attributes_escape([{:title, "the title"}, {:id, "the id"}]) |> safe_to_string() == + ~s( title="the title" id="the id") + end + + test "handle nested data" do + assert attributes_escape([{"data", [a: "1", b: "2"]}]) |> safe_to_string() == + ~s( data-a="1" data-b="2") + + assert attributes_escape([{"aria", [a: "1", b: "2"]}]) |> safe_to_string() == + ~s( aria-a="1" aria-b="2") + end + + test "handle class value as string" do + assert attributes_escape([{:class, "btn"}]) |> safe_to_string() == ~s( class="btn") + end + + test "handle class value as list" do + assert attributes_escape([{:class, ["btn", nil, false, "active"]}]) |> safe_to_string() == + ~s( class="btn active") + end + + test "handle class key as string" do + assert attributes_escape([{"class", "btn"}]) |> safe_to_string() == ~s( class="btn") + end + + test "supress attribute when value is falsy" do + assert attributes_escape([{"title", nil}]) |> safe_to_string() == ~s() + assert attributes_escape([{"title", false}]) |> safe_to_string() == ~s() + end + + test "supress value when value is true" do + assert attributes_escape([{"selected", true}]) |> safe_to_string() == ~s( selected) + end + end + test "tag" do assert tag(:br) |> safe_to_string() == ~s(
) @@ -20,8 +75,12 @@ defmodule Phoenix.HTML.TagTest do ~s() assert tag(:input, my_attr: "blah") |> safe_to_string() == ~s() - assert tag(:input, [{"my_<_attr", "blah"}]) |> safe_to_string() == ~s() - assert tag(:input, [{{:safe, "my_<_attr"}, "blah"}]) |> safe_to_string() == ~s() + + assert tag(:input, [{"my_<_attr", "blah"}]) |> safe_to_string() == + ~s() + + assert tag(:input, [{{:safe, "my_<_attr"}, "blah"}]) |> safe_to_string() == + ~s() assert tag(:input, data: [my_attr: "blah"]) |> safe_to_string() == ~s()