diff --git a/lib/beacon/jason.ex b/lib/beacon/jason.ex
new file mode 100644
index 00000000..7cd859ef
--- /dev/null
+++ b/lib/beacon/jason.ex
@@ -0,0 +1,3 @@
+defimpl Jason.Encoder, for: Tuple do
+ defdelegate encode(value, opts), to: Beacon.Template.HEEx.JSONEncoder, as: :encode_eex_block
+end
diff --git a/lib/beacon/template/heex/heex_decoder.ex b/lib/beacon/template/heex/heex_decoder.ex
index 63bcc808..5f3f30ad 100644
--- a/lib/beacon/template/heex/heex_decoder.ex
+++ b/lib/beacon/template/heex/heex_decoder.ex
@@ -55,10 +55,15 @@ defmodule Beacon.Template.HEEx.HEExDecoder do
["<%!--", content, "--%>"]
end
- defp transform_node(%{"tag" => "eex_block", "blocks" => blocks, "arg" => arg}) do
+ defp transform_node(%{"tag" => "eex_block", "arg" => arg, "blocks" => blocks}) do
["<%=", arg, " %>", Enum.map(blocks, &transform_block/1)]
end
+ # eex_block with a `for` comprehension
+ defp transform_node(%{"tag" => "eex_block", "arg" => arg, "ast" => ast}) do
+ ["<%= ", arg, " %>", decode_comprehension_ast(ast)]
+ end
+
defp transform_node(%{"tag" => tag, "attrs" => %{"self_close" => true} = attrs, "content" => []}) do
attrs = Map.delete(attrs, "self_close")
["<", tag, " ", transform_attrs(attrs), "/>"]
@@ -129,4 +134,25 @@ defmodule Beacon.Template.HEEx.HEExDecoder do
defp reconstruct_attr({name, {:expr, content, _}, _}) do
[name, "=", ?{, content, ?}]
end
+
+ defp decode_comprehension_ast(ast) do
+ ast
+ |> Jason.decode!()
+ |> List.flatten()
+ |> Enum.reduce([], fn node, acc -> decode_comprehension_ast_node(node, acc) end)
+ |> Enum.reverse()
+ end
+
+ defp decode_comprehension_ast_node(%{"type" => "text", "content" => [content | _]}, acc) do
+ [content | acc]
+ end
+
+ defp decode_comprehension_ast_node(%{"type" => "eex", "content" => [expr, %{"opt" => ~c"="}]}, acc) do
+ [["<%= ", expr, " %>"] | acc]
+ end
+
+ # TODO: improve eex_block closing detection (maybe augment it in JSONEncoder)
+ defp decode_comprehension_ast_node("end", acc), do: ["<% end %>" | acc]
+
+ defp decode_comprehension_ast_node(node, acc) when is_binary(node), do: [node | acc]
end
diff --git a/lib/beacon/template/heex/json_encoder.ex b/lib/beacon/template/heex/json_encoder.ex
index c5a9b5a7..4591da5c 100644
--- a/lib/beacon/template/heex/json_encoder.ex
+++ b/lib/beacon/template/heex/json_encoder.ex
@@ -44,7 +44,6 @@ defmodule Beacon.Template.HEEx.JSONEncoder do
Note that:
* `rendered_html` key is optional
- * Comprehensions are not supported
## Example
@@ -91,16 +90,7 @@ defmodule Beacon.Template.HEEx.JSONEncoder do
end
rescue
exception ->
- message = """
- failed to encode the HEEx template
-
- Got:
-
- #{Exception.message(exception)}
-
- """
-
- reraise Beacon.ParserError, [message: message], __STACKTRACE__
+ {:error, Exception.message(exception)}
end
defp encode_tokens(ast, site, assigns) do
@@ -129,7 +119,7 @@ defmodule Beacon.Template.HEEx.JSONEncoder do
defp transform([], acc, _site, _assigns), do: acc
- # Strips blank text nodes and insignificant whitespace before or after text.
+ # strips out blank text nodes and insignificant whitespace before or after text.
defp transform_entry({:text, text, _}, _site, _assigns) do
cond do
:binary.first(text) in ~c"\n" or :binary.last(text) in ~c"\n" ->
@@ -153,12 +143,24 @@ defmodule Beacon.Template.HEEx.JSONEncoder do
}
end
- defp transform_entry({:eex_block, arg, content}, site, assigns) do
- %{
- "tag" => "eex_block",
- "arg" => arg,
- "blocks" => Enum.map(content, fn block -> transform_block(block, site, assigns) end)
- }
+ defp transform_entry({:eex_block, arg, content} = entry, site, assigns) do
+ arg = String.trim(arg)
+
+ # TODO: improve for comprehensions detection
+ if String.starts_with?(arg, "for ") do
+ %{
+ "tag" => "eex_block",
+ "arg" => arg,
+ "rendered_html" => render_comprehension_block(site, assigns, entry),
+ "ast" => encode_comprehension_block(entry)
+ }
+ else
+ %{
+ "tag" => "eex_block",
+ "arg" => arg,
+ "blocks" => Enum.map(content, fn block -> transform_block(block, site, assigns) end)
+ }
+ end
end
defp transform_entry({:eex_comment, text}, _site, _assigns) do
@@ -241,4 +243,96 @@ defmodule Beacon.Template.HEEx.JSONEncoder do
"content" => encode_tokens(content, site, assigns)
}
end
+
+ defp encode_comprehension_block({:eex_block, _arg, content}) do
+ case Jason.encode(content) do
+ {:ok, json} -> json
+ error -> error
+ end
+ end
+
+ defp render_comprehension_block(site, assigns, {:eex_block, arg, nodes}) do
+ arg = ["<%= ", arg, " %>", "\n"]
+
+ template =
+ Enum.reduce(nodes, [arg], fn node, acc ->
+ [[extract_node_text(node), " \n "] | acc]
+ end)
+ |> Enum.reverse()
+ |> List.to_string()
+
+ Beacon.Template.HEEx.render(site, template, assigns)
+ end
+
+ defp extract_node_text({nodes, "end"} = value) when is_list(nodes) do
+ value
+ |> Tuple.to_list()
+ |> Enum.reduce([], fn node, acc -> [extract_node_text(node) | acc] end)
+ |> Enum.reverse()
+ end
+
+ defp extract_node_text(value) when is_list(value) do
+ value
+ |> Enum.reduce([], fn node, acc -> [extract_node_text(node) | acc] end)
+ |> Enum.reverse()
+ end
+
+ defp extract_node_text("end" = _value), do: ["<% end %>"]
+
+ defp extract_node_text(value) when is_binary(value), do: value
+
+ defp extract_node_text({:text, text, _}), do: text
+
+ defp extract_node_text({:html_comment, children}), do: [extract_node_text(children), "\n"]
+
+ # TODO: eex comments are stripped out of rendered html by the heex engine
+ defp extract_node_text({:eex_comment, _content}), do: []
+
+ defp extract_node_text({:eex, expr, %{opt: ~c"="}}), do: ["<%= ", expr, " %>"]
+
+ defp extract_node_text({:eex, expr, _}), do: ["<% ", expr, " %>"]
+
+ defp extract_node_text({:eex_block, expr, children}), do: ["<%= ", expr, " %>", extract_node_text(children)]
+
+ defp extract_node_text({:tag_self_close, tag, attrs}) do
+ attrs =
+ Enum.reduce(attrs, [], fn attr, acc ->
+ [extract_node_attr(attr) | acc]
+ end)
+
+ [?<, tag, " ", attrs, "/>"]
+ end
+
+ defp extract_node_text({:tag_block, tag, _, children, _}) do
+ [?<, tag, ?>, extract_node_text(children), "", tag, ">"]
+ end
+
+ defp extract_node_attr({attr, {:string, text, _}, _}), do: [attr, ?=, ?", text, ?", " "]
+ defp extract_node_attr({attr, {:expr, expr, _}, _}), do: [attr, ?=, ?{, expr, ?}, " "]
+
+ def encode_eex_block(value) when is_tuple(value) do
+ value
+ |> Tuple.to_list()
+ |> Enum.reduce([], fn node, acc -> [encode_block_node(node) | acc] end)
+ |> Enum.reverse()
+ end
+
+ def encode_eex_block(value, opts) when is_tuple(value) do
+ value
+ |> encode_eex_block()
+ |> Jason.Encode.list(opts)
+ end
+
+ def encode_block_node(nodes) when is_list(nodes) do
+ nodes
+ |> Enum.reduce([], fn node, acc -> [encode_block_node(node) | acc] end)
+ |> Enum.reverse()
+ end
+
+ def encode_block_node(node) when is_tuple(node) do
+ [type | content] = Tuple.to_list(node)
+ %{type: type, content: content |> List.flatten() |> encode_block_node()}
+ end
+
+ def encode_block_node(node), do: node
end
diff --git a/lib/beacon_web/controllers/api/page_json.ex b/lib/beacon_web/controllers/api/page_json.ex
index 655ea353..43f954aa 100644
--- a/lib/beacon_web/controllers/api/page_json.ex
+++ b/lib/beacon_web/controllers/api/page_json.ex
@@ -34,8 +34,11 @@ defmodule BeaconWeb.API.PageJSON do
defp page_ast(page) do
path = for segment <- String.split(page.path, "/"), segment != "", do: segment
beacon_live_data = Beacon.DataSource.live_data(page.site, path, [])
- {:ok, ast} = Beacon.Template.HEEx.JSONEncoder.encode(page.site, page.template, %{beacon_live_data: beacon_live_data})
- ast
+
+ case Beacon.Template.HEEx.JSONEncoder.encode(page.site, page.template, %{beacon_live_data: beacon_live_data}) do
+ {:ok, ast} -> ast
+ _ -> []
+ end
end
defp maybe_include_layout(%{template: page_template} = data, %Page{layout: %Layout{} = layout}) do
diff --git a/test/beacon/template/heex/heex_decoder_test.exs b/test/beacon/template/heex/heex_decoder_test.exs
index 26866dd2..8c3d2835 100644
--- a/test/beacon/template/heex/heex_decoder_test.exs
+++ b/test/beacon/template/heex/heex_decoder_test.exs
@@ -51,6 +51,17 @@ defmodule Beacon.Template.HEEx.HEExDecoderTest do
""")
end
+ test "comprehensions" do
+ assert_equal(
+ ~S"""
+ <%= for val <- @beacon_live_data[:vals] do %>
+ <%= val %>
+ <% end %>
+ """,
+ %{beacon_live_data: %{vals: [1]}}
+ )
+ end
+
test "function components" do
assert_equal(~S|
<%= user.name %>
+ test "inline expression" do + assert Tokenizer.tokenize(~S| + <%= user.name %> + |) == {:ok, [{:eex, "user.name", %{line: 2, opt: ~c"=", column: 7}}]} + end + + test "block expression" do + assert Tokenizer.tokenize(~S| <%= if true do %>this
<% else %>that
<% end %> -