スポンサーリンク

2015年9月26日

[Elixir]HTMLを生成するモジュールを作りたい (失敗編)

とある錬金術師の万能薬(Elixir)

Goal

HTML生成モジュールを作成する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5

Wait a minute

最初に書いておきます。失敗談の記事です。
Phoenix_HTMLライブラリを読んで、
HTMLの生成を自分で実装してみようと思った。
今回の内容だと、何かの役に立つって内容ではないです。
検証用なのでかなり大雑把に作っています。

Index

Generate HTML

Generate HTML

最初のステップは、ただpタグの文字列を返すだけです。

first step

defmodule GenerateHtml do
  def p_tag do
    "<p>"
  end
end

iex> GenerateHtml.p_tag
"<p>"
次は、アトムを文字列に変換して出力します。

2th step

defmodule GenerateHtml do
  def tag(:p) do
    "<#{:p}>"
  end
end

iex> GenerateHtml.tag(:p)
"<p>"
少し汎用的にタグの生成をできるようにします。

3th step

defmodule GenerateHtml do
  def tag(name) when is_atom(name) do
    "<#{name}>"
  end

  def close_tag(name) when is_atom(name) do
    "</#{name}>"
  end
end

iex> GenerateHtml.tag(:p)
"<p>"
iex> GenerateHtml.close_tag(:p)
"</p>"
CSSのclassを指定できるようにします。

4th step

defmodule GenerateHtml do
  def tag(name) when is_atom(name) do
    tag(name, [])
  end

  def tag(name, attrs) when is_atom(name) and is_list(attrs) do
    "<#{name} #{:class}=\"#{Keyword.get_values(attrs, :class)}\">"
  end

  def close_tag(name) when is_atom(name) do
    "</#{name}>"
  end
end

iex> GenerateHtml.tag(:p, class: "hoge")
"<p class=\"hoge\">"
このままでは、classの指定しかできません。
タグのオプションやidなど他にも指定できた方が良いものが多くありますね。

5th step

defmodule GenerateHtml do
  def tag(name) when is_atom(name) do
    tag(name, [])
  end

  def tag(name, attrs) when is_atom(name) and is_list(attrs) do
    "<#{name}#{build_attrs(attrs)}>"
  end

  def close_tag(name) when is_atom(name) do
    "</#{name}>"
  end

  defp build_attrs(attrs) when is_list(attrs) do
    Enum.reduce(attrs, "", &attrs_mapper/2)
  end

  defp attrs_mapper({key, value}, acc) when is_atom(value) or is_binary(value),
    do: acc <> " #{key}=\"#{value}\""
  defp attrs_mapper({key, value}, acc) when is_list(value),
    do: attrs_mapper({key, Enum.join(value, " ")}, acc)
end

iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], id: :foo)
"<p class=\"hoge huge\" id=\"foo\">"
data-[name]と言ったように-(ハイフン)で区切られたオプションを指定したい場合はどうしましょう?
実は変数名やアトムで-(ハイフン)を使うとエラーになります。
こんな感じに…
iex> data-type = "value"
** (CompileError) iex:3: illegal pattern

iex> data_type = :data-type
** (RuntimeError) undefined function: type/0
キーとバリューの値にあたる部分をタプルにして、ネストとして処理してみます。

6th step

defmodule GenerateHtml do
  def tag(name) when is_atom(name) do
    tag(name, [])
  end

  def tag(name, attrs) when is_atom(name) and is_list(attrs) do
    "<#{name}#{build_attrs(attrs)}>"
  end

  def close_tag(name) when is_atom(name) do
    "</#{name}>"
  end

  defp build_attrs(attrs) when is_list(attrs) do
    Enum.reduce(attrs, "", &attrs_mapper/2)
  end

  defp attrs_mapper({key, value}, acc) when is_atom(value) or is_binary(value),
    do: acc <> " #{key}=\"#{value}\""
  defp attrs_mapper({key, value}, acc) when is_list(value),
    do: attrs_mapper({key, Enum.join(value, " ")}, acc)
  defp attrs_mapper({key, {nest_key, value}}, acc),
    do: attrs_mapper({"#{key}-#{nest_key}", value}, acc)
end

iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value"})
"<p class=\"hoge huge\" data-type=\"value\">"
valueの部分にtrue、false、nilが来た場合の処理を追加します。
関数の定義している順番にパターンマッチしているようなので、定義する順番に気を付けて下さい。

7th step

defmodule GenerateHtml do
  def tag(name) when is_atom(name) do
    tag(name, [])
  end

  def tag(name, attrs) when is_atom(name) and is_list(attrs) do
    "<#{name}#{build_attrs(attrs)}>"
  end

  def close_tag(name) when is_atom(name) do
    "</#{name}>"
  end

  defp build_attrs(attrs) when is_list(attrs) do
    Enum.reduce(attrs, "", &attrs_mapper/2)
  end

  defp attrs_mapper({key, true}, acc),
    do: attrs_mapper({key, key}, acc)
  defp attrs_mapper({_key, false}, acc),
    do: acc
  defp attrs_mapper({_key, nil}, acc),
    do: acc
  defp attrs_mapper({key, value}, acc) when is_atom(value) or is_binary(value),
    do: acc <> " #{key}=\"#{value}\""
  defp attrs_mapper({key, value}, acc) when is_list(value),
    do: attrs_mapper({key, Enum.join(value, " ")}, acc)
  defp attrs_mapper({key, {nest_key, value}}, acc),
    do: attrs_mapper({"#{key}-#{nest_key}", value}, acc)
end

iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value"}, hoge: true, huge: false, foo: nil, bar: true)
"<p class=\"hoge huge\" data-type=\"value\" hoge=\"hoge\" bar=\"bar\">"
ここまでやって、ようやく気が付きました。
再帰で処理しないとだめだこれ…っとorz
とりあえず今回はここまで~
値の部分がネストのネストになっていたらどうするかなど、対応しないといけないパターンが他にもある。
それにリストとタプルが混在していて分かり辛いので、リストの方へ統一しようと思います。
ちなみに、以下のようなパターンに対応していない。
iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value", "value2"}, hoge: true, huge: false, foo: nil, bar: true)
** (FunctionClauseError) no function clause matching in GenerateHtml.attrs_mapper/2
    (generate_html_dsl) lib/generate_html.ex:18: GenerateHtml.attrs_mapper({:data, {:type, "value", "value2"}}, " class=\"hoge huge\"")
               (elixir) lib/enum.ex:1261: Enum."-reduce/3-lists^foldl/2-0-"/3
    (generate_html_dsl) lib/generate_html.ex:7: GenerateHtml.tag/2

iex> GenerateHtml.tag(:p, class: ["hoge", "huge"], data: {:type, "value", :input, "value2"}, hoge: true, huge: false, foo: nil, bar: true)
** (FunctionClauseError) no function clause matching in GenerateHtml.attrs_mapper/2
    (generate_html_dsl) lib/generate_html.ex:18: GenerateHtml.attrs_mapper({:data, {:type, "value", :input, "value2"}}, " class=\"hoge huge\"")
               (elixir) lib/enum.ex:1261: Enum."-reduce/3-lists^foldl/2-0-"/3
    (generate_html_dsl) lib/generate_html.ex:7: GenerateHtml.tag/2
そういうわけで結局、再帰的に処理をする形に修正すると思います。

Speaking to oneself

とりあえず、やってみたが…
何故、Phoenix_HTMLライブラリでは再帰で処理しているのか理解した。
設計思想の一部でも理解できたので、悪くはないでしょう。
最終的に何がやりたいのかって話ですが…
ページネーション用のHTML生成に使えたらな~ってのと、EExのEngineについて知りたかった。
追々、記事にできたらします。ノシ

Bibliography

人気の投稿