Current section
Files
Jump to
Current section
Files
lib/template/evaluate.ex
defmodule Dragon.Template.Evaluate do
use Dragon.Context
import Dragon.Tools
import Dragon.Template.Read
@moduledoc """
Core heart of evaluating EEX Templates.
Although Dragon is a standalone genserver for its data, we still try to push
the Dragon struct on the current processes' stack to keep data movement to
a minimum, and bring it in again with Dragon.get() only when we've lost the
context (such as when called from within a template's helper functions).
"""
def all(%Dragon{files: %{dragon: l}} = d), do: all(d, Map.keys(l))
def all(%Dragon{} = d, [file | rest]) do
with {:ok, path} <- find_file(d.root, file) do
read_template_header(path)
|> evaluate(:primary, d)
|> validate()
|> commit_file()
all(d, rest)
end
end
def all(%Dragon{} = d, _), do: {:ok, d}
##############################################################################
def processing(file, layout \\ nil)
def processing(file, nil), do: notify([:green, "EEX Template ", :reset, :bright, file])
def processing(file, layout),
do:
notify([:green, "EEX Template ", :reset, :bright, file, :reset, :light_blue, " (#{layout})"])
##############################################################################
def evaluate(read_result, type, dragon, args \\ [])
# process files with a layout directive slightly differently. First, process
# the current file and get the output results. Then call, as an include,
# the layout template, sending the current output into that as a page
# argument (@page.content)
def evaluate(
{:ok, %{"@spec": %{layout: layout}} = headers, path, offset},
:primary,
%Dragon{} = d,
args
) do
processing(path, layout)
with {:ok, content} <- read_template_body(path, offset),
{:ok, env} <- Dragon.Template.Env.get_for(path, :primary, headers, d, args),
{:ok, output} <- evaluate_template(d, path, content, env),
# posteval first, before insertion by layout; for markdown/etc
{:ok, target, headers, output} <- posteval(d, headers, path, output),
# then insert into layout
{:ok, _, _, output} <- include_file(Path.join(d.layouts, "_#{layout}"), d, :layout,
content: output,
parent: headers
),
do: {:ok, target, headers, output}
end
def evaluate({:ok, headers, path, offset}, type, %Dragon{} = d, args) do
if type != :layout do
processing(path)
end
with {:ok, content} <- read_template_body(path, offset),
{:ok, env} <- Dragon.Template.Env.get_for(path, type, headers, d, args),
{:ok, output} <- evaluate_template(d, path, content, env),
do: posteval(d, headers, path, output)
end
def evaluate({:error, reason}, _, _, _), do: abort("Unable to continue: #{reason}")
################################################################################
# we don't pay attention to layout here
def include_file(path, %Dragon{} = d, _, args) do
case find_file(d.root, path) do
{:ok, target} ->
info("+ Including #{target}")
read_template_header(target)
|> handle_non_template(target)
|> evaluate(:layout, d, args)
{:error, msg} ->
abort("+ Include failed: #{msg}")
end
end
def handle_non_template({:error, _}, target), do: {:ok, %{}, target, 0}
def handle_non_template({:ok, _, _, _} = pass, _), do: pass
################################################################################
def validate({:ok, dst, headers, content}) do
# future: scan html content for breaks
{:ok, dst, headers, content}
end
##############################################################################
def posteval(%{root: root, build: build} = d, headers, origin, content) do
target = Path.join(build, Dragon.Tools.drop_root(root, origin))
Dragon.Plugin.posteval(d, origin, target, headers, content)
end
##############################################################################
def commit_file({:ok, path, headers, content}) do
info([:light_black, " Saving ", :reset, path])
file =
case headers do
%{"@spec": %{output: "folder/index"}} -> Path.join(Path.rootname(path), "index.html")
_ -> path
end
Dragon.Tools.write_file(file, content)
end
# side-effect execution frame state management
defp with_frame(update, inner) do
try do
update.(Dragon.frame_head()) |> Dragon.frame_push() |> inner.()
after
Dragon.frame_pop()
end
end
##############################################################################
defp evaluate_template(%Dragon{imports: imports}, path, template, env) do
with_frame(
fn prev ->
case prev do
nil -> %{prev: nil, this: path, top: path}
%{this: prev} = frame -> %{frame | prev: prev, this: path}
end
end,
fn frame ->
evaluate_frame(frame, imports, path, template, env)
end
)
end
defp evaluate_frame(_frame, imports, path, template, env) do
try do
{:ok, EEx.eval_string(imports <> template, assigns: Map.to_list(env))}
rescue
err ->
case err do
## TODO: include offset in line count so you can find it in the editor!
%CompileError{file: "nofile", line: line, description: msg} ->
# minus one to the line because we added a line above
abort_nofile_error(template, path, line - 1, msg)
error ->
error("Error processing #{path}")
Kernel.reraise(error, __STACKTRACE__)
end
end
end
def abort_nofile_error(template, path, lineno, msg) do
first = lineno - 2
first = if first < 0, do: 0, else: first
last = lineno + 2
stderr(["\n", :yellow, "? ", "#{path}:#{lineno}", :reset, " — ", :yellow, :bright, msg, "\n"])
String.split(template, "\n")
|> Enum.reduce_while(1, fn line, index ->
cond do
index == lineno -> print_with_line("»", index, line)
index > first and index < last -> print_with_line(" ", index, line)
true -> :ok
end
if index == last do
{:halt, index}
else
{:cont, index + 1}
end
end)
IO.puts(:stderr, "\n")
abort("Cannot continue")
end
defp print_with_line(prefix, index, line) do
padded = String.pad_leading("#{index}", 3)
IO.puts(:stderr, IO.ANSI.format([:blue, :bright, "#{prefix}#{padded}: ", :reset, line]))
end
end