defmodule SnapFramework.Engine do
@moduledoc """
The EEx template engine.
# Overview
The SnapFramework Engine is responsible for parsing EEx templates and building graphs from them.
You should always start a template with the a graph, and then add any components and primitives immediately after it.
``` elixir
<%= graph font_size: 20 %>
<%= component Scenic.Component.Button,
"button text",
id: :btn
%>
<%= primitive Scenic.Primitive.Rectangle,
{100, 100},
id: :rect,
fill: :steel_blue
%>
```
In the above example you can see how simple it is to render component and primitives.
# Layouts
The templating engine also supports layouts.
``` elixir
<%= graph font_size: 20 %>
<%= layout padding: 100, width: 600, height: 600, translate: {100, 10} do %>
<%= component Scenic.Component.Button, "test btn", id: :test_btn %>
<%= component Scenic.Component.Button, "test btn", id: :test_btn %>
<%= component Scenic.Component.Button, "test btn", id: :test_btn %>
<%= component Scenic.Component.Button, "test btn", id: :test_btn %>
<%= component Scenic.Component.Button, "test btn", id: :test_btn %>
<%= component Scenic.Component.Button, "test btn", id: :test_btn %>
<%= layout padding: 0, width: 600, height: 300, translate: {10, 10} do %>
<%= component Scenic.Component.Input.Dropdown, {
@dropdown_opts,
@dropdown_value
},
id: :dropdown_1,
z_index: 100
%>
<%= component Scenic.Component.Input.Dropdown, {
@dropdown_opts,
@dropdown_value
},
id: :dropdown_2
%>
<% end %>
<% end %>
```
The only required options on templates are `width` and `height`.
Any nested layouts will have the padding and translate of the previous layout added onto it.
Any components rendered within a layout are added directly to the graph. Which means you can modify them directly in the scene you're working in.
There is no parent component that is rendered.
"""
@behaviour EEx.Engine
require Logger
def encode_to_iodata!({:safe, body}), do: body
def encode_to_iodata!(body) when is_binary(body), do: body
def compile(path, assigns, info, _env) do
quoted = EEx.compile_file(path, info)
{result, _binding} = Code.eval_quoted(quoted, assigns)
result
end
def compile_string(string, assigns, info, env) do
quoted = EEx.compile_string(string, info)
{result, _binding} = Code.eval_quoted(quoted, assigns, env)
result
end
@doc false
def init(opts) do
%{
iodata: [],
dynamic: [],
vars_count: 0,
assigns: opts[:assigns] || []
}
end
@doc false
def handle_begin(state) do
Macro.var(:assigns, __MODULE__)
%{state | iodata: [], dynamic: []}
end
@doc false
def handle_end(quoted) do
quoted
|> handle_body()
end
@doc false
def handle_body(state) do
%{iodata: iodata, dynamic: dynamic} = state
safe = Enum.reverse(iodata)
{:__block__, [], Enum.reverse([safe | dynamic])}
end
@doc false
def handle_text(state, text) do
handle_text(state, [], text)
end
@doc false
def handle_text(state, _meta, text) do
%{iodata: iodata} = state
%{state | iodata: [text | iodata]}
end
@doc false
def handle_expr(state, "=", ast) do
ast = traverse(ast, state.assigns)
%{iodata: iodata, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(:"arg#{vars_count}", __MODULE__)
ast = quote do: unquote(var) = unquote(ast)
%{state | dynamic: [ast | dynamic], iodata: [var | iodata], vars_count: vars_count + 1}
end
def handle_expr(state, "", ast) do
ast = traverse(ast, state.assigns)
%{dynamic: dynamic} = state
%{state | dynamic: [ast | dynamic]}
end
def handle_expr(state, marker, ast) do
EEx.Engine.handle_expr(state, marker, ast)
end
## Traversal
defp traverse(expr, assigns) do
expr
|> Macro.prewalk(&SnapFramework.Parser.Assigns.run(&1, assigns))
# |> Macro.prewalk(&SnapFramework.Parser.Enumeration.run/1)
|> Macro.prewalk(&SnapFramework.Parser.Layout.run/1)
|> Macro.prewalk(&SnapFramework.Parser.Graph.run/1)
|> Macro.prewalk(&SnapFramework.Parser.Component.run/1)
|> Macro.prewalk(&SnapFramework.Parser.Primitive.run/1)
# |> Macro.prewalk(&SnapFramework.Parser.Outlet.run(&1, assigns))
end
@doc false
def fetch_assign!(assigns, key) do
case Access.fetch(assigns, key) do
{:ok, val} ->
val
:error ->
raise ArgumentError, """
assign @#{key} not available in eex template.
Please make sure all proper assigns have been set. If this
is a child template, ensure assigns are given explicitly by
the parent template as they are not automatically forwarded.
Available assigns: #{inspect(Enum.map(assigns, &elem(&1, 0)))}
"""
end
end
end