defmodule DocxTmpl.Interpreter do
@moduledoc false
# Render a parsed AST against assigns. Pure: ast + map -> binary.
@spec render(list(), map()) :: binary()
def render(ast, assigns) when is_list(ast) and is_map(assigns) do
render_nodes(ast, [assigns])
end
defp render_nodes(nodes, scopes) do
nodes
|> Enum.map(&render_node(&1, scopes))
|> IO.iodata_to_binary()
end
defp render_node({:text, t}, _scopes), do: t
defp render_node({:var, path}, scopes) do
scopes |> lookup_scoped(path) |> stringify() |> xml_escape()
end
defp render_node({:if, path, children}, scopes) do
if truthy?(lookup_scoped(scopes, path)),
do: render_nodes(children, scopes),
else: ""
end
defp render_node({:unless, path, children}, scopes) do
if truthy?(lookup_scoped(scopes, path)),
do: "",
else: render_nodes(children, scopes)
end
defp render_node({:each, path, children}, scopes) do
case lookup_scoped(scopes, path) do
list when is_list(list) ->
Enum.map(list, fn item -> render_nodes(children, [item_scope(item) | scopes]) end)
_ ->
""
end
end
defp item_scope(item) when is_map(item), do: Map.put(item, "this", item)
defp item_scope(item), do: %{"this" => item}
defp lookup_scoped(scopes, path) do
parts = String.split(path, ".")
Enum.find_value(scopes, fn scope ->
case fetch_path(scope, parts) do
{:ok, v} -> {:found, v}
:error -> nil
end
end)
|> case do
{:found, v} -> v
nil -> nil
end
end
defp fetch_path(value, []), do: {:ok, value}
defp fetch_path(map, [head | rest]) when is_map(map) do
cond do
Map.has_key?(map, head) -> fetch_path(Map.get(map, head), rest)
Map.has_key?(map, safe_atom(head)) -> fetch_path(Map.get(map, safe_atom(head)), rest)
true -> :error
end
end
defp fetch_path(_, _), do: :error
defp safe_atom(s) do
String.to_existing_atom(s)
rescue
ArgumentError -> :__docx_tmpl_missing__
end
defp truthy?(nil), do: false
defp truthy?(false), do: false
defp truthy?(""), do: false
defp truthy?([]), do: false
defp truthy?(_), do: true
defp stringify(nil), do: ""
defp stringify(v) when is_binary(v), do: v
defp stringify(v), do: to_string(v)
defp xml_escape(s) do
s
|> String.replace("&", "&")
|> String.replace("<", "<")
|> String.replace(">", ">")
end
end