defmodule Dragon do
@moduledoc """
Dragon is meant to be run standalone. See [README](https://github.com/srevenant/dragon) in root of project for full
details on how it works.
"""
use GenServer
use Dragon.Context
import Dragon.Tools.File.WalkTree
import Dragon.Tools.File, only: [scan_file: 3]
@always_imports [
"Dragon.Template.Functions",
"Dragon.English",
"Transmogrify",
"Transmogrify.As"
]
@always_plugins [
%{when: "posteval", module: "Dragon.Plugin.Markdown"}
]
defstruct root: ".",
build: "_build",
layouts: ["_lib/layout"],
data_paths: %{},
data: nil,
imports: @always_imports,
plugins: @always_plugins,
files: %{},
opts: %{},
# execution frames
frames: []
@type t :: %Dragon{
root: String.t(),
build: String.t(),
layouts: list(String.t()),
data_paths: map(),
data: list(map) | map() | nil,
imports: list(String.t()) | String.t(),
plugins: list(map()) | map(),
files: %{(path :: String.t()) => atom()},
opts: %{atom() => any()},
frames: list()
}
def start_link(init), do: GenServer.start_link(__MODULE__, init, name: __MODULE__)
##############################################################################
# @spec init(target :: String.t()) :: Dragon.t()
def init(args) do
stdout([:light_blue, :bright, "Starting Dragon CMS"])
{:ok, struct(__MODULE__, args)}
end
##############################################################################
def configure(root) when is_binary(root),
do: GenServer.call(__MODULE__, {:configure, root})
def get(), do: GenServer.call(__MODULE__, :get)
def get(key), do: GenServer.call(__MODULE__, {:get, key})
def get!() do
with {:ok, value} <- get(), do: value
end
def get!(key) do
with {:ok, value} <- get(key), do: value
end
# frame / execution stack management
def frame_push(value), do: GenServer.call(__MODULE__, {:frame_push, value})
def frame_pop(), do: GenServer.call(__MODULE__, :frame_pop)
def frame_head(), do: GenServer.call(__MODULE__, :frame_head)
##############################################################################
# note: Dragon state is immutable after being initialized. It's only a
# standalone server/process for easy reference when processes lose context
@doc false
def handle_call({:configure, root}, _, state) do
Application.put_env(:dragon, :from, root)
case Dragon.Tools.File.find_index_file(root, @config_file) do
{:ok, path} ->
case YamlElixir.read_all_from_file(path) do
{:ok, [%{"version" => 1.0} = config]} ->
root = Path.dirname(path)
dragon =
struct(__MODULE__, Transmogrify.transmogrify(config))
|> update_paths(root)
|> update_imports()
|> update_plugins()
try do
Dragon.Data.load_data(dragon)
rescue
error in Dragon.AbortError ->
{:error, error.message}
end
|> case do
{:error, msg} -> {:reply, {:error, msg}, state}
{:ok, %Dragon{} = dragon} -> {:reply, {:ok, dragon}, dragon}
end
{:error, %{message: msg}} ->
{:reply, {:error, "Error reading Dragon Config (#{path}): #{msg}"}, state}
err ->
IO.inspect(err, label: "Error")
{:reply, {:error, "Unable to find valid Dragon config in #{path}"}, state}
end
{:error, reason} ->
{:reply, {:error, "Unable to find target site/config file '#{root}': #{reason}"}, state}
end
end
def handle_call(:get, _, state), do: {:reply, {:ok, state}, state}
def handle_call({:get, key}, _, state), do: {:reply, {:ok, Map.get(state, key)}, state}
# keep it in the dragon struct, but if this gets too big we can move it to
# a separate process
def handle_call({:frame_push, value}, _, state),
do: {:reply, value, %Dragon{state | frames: [value | state.frames]}}
def handle_call(:frame_pop, _, state) do
{:reply, :ok,
%Dragon{
state
| frames:
case state.frames do
[] -> []
[_ | frames] -> frames
end
}}
end
def handle_call(:frame_head, _, %{frames: [head | _]} = state),
do: {:reply, head, state}
def handle_call(:frame_head, _, %{frames: []} = state),
do: {:reply, nil, state}
##############################################################################
defp update_paths(%Dragon{build: build} = d, root),
do: %Dragon{d | root: root, build: Path.join(root, build)}
defp update_imports(%Dragon{imports: imports} = d) when is_list(imports) do
imports =
MapSet.new(@always_imports ++ imports)
|> MapSet.to_list()
|> Enum.map(&"import #{&1}")
|> Enum.join(";")
%Dragon{d | imports: "<% #{imports} %>"}
end
defp update_plugins(%Dragon{plugins: plugs} = d) when is_list(plugs),
do: %Dragon{d | plugins: prepare_plugins(plugs, %{posteval: []})}
defp prepare_plugins(
[%{when: "posteval", module: name} | rest],
%{posteval: list} = plugs
) do
# TODO: check plugin module exists; put into struct
prepare_plugins(rest, %{plugs | posteval: [String.to_atom("Elixir.#{name}") | list]})
end
defp prepare_plugins([nope | _], _) do
error("Invalid plugin detected")
IO.inspect(nope, label: "PLUGIN")
abort("Cannot continue")
end
defp prepare_plugins([], out), do: Map.new(out, fn {k, v} -> {k, Enum.reverse(v)} end)
##############################################################################
def prepare_build(%Dragon{} = dragon) do
stdout([:green, "Creating build folder: ", :reset, :bright, dragon.build])
case File.mkdir_p(dragon.build) do
{:error, reason} ->
abort("Unable to make build folder '#{dragon.build}': #{reason}")
:ok ->
nil
end
with %Dragon{} = dragon <- walk_tree(dragon, "", no_match: &scan_file/3),
do: Dragon.Tools.File.Synchronize.synchronize(dragon)
end
end