defmodule Dragon.Template.Functions do @moduledoc """ Helper functions for Dragon Templates. """ import Dragon.Tools.File, only: [drop_root: 3, find_file: 2] use Dragon.Context import Rivet.Utils.Cli.Print import Dragon.Template.Evaluate, only: [evaluate_template: 4] ############################################################################## def include(path, args \\ []) do with {:ok, path, _} <- fix_path(path) do case Dragon.Template.Evaluate.include_file(path, Dragon.get!(), :inline, args) do {:error, error} -> abort(error) {:ok, _, _, content} -> content end end end ############################################################################## def markdownify(nil), do: "" def markdownify(x) when is_number(x), do: x def markdownify(content) do case Earmark.as_html(content) do {:ok, content, _} -> content {:error, msg, deets} -> raise Dragon.AbortError, "markdownify: #{msg}; #{inspect(deets)}" end end ############################################################################## def jsonify(content), do: Jason.encode!(content) # Note: eex doesn't run plugins, by design, because plugins can run this def eex(content) do with {:ok, d} <- Dragon.get(), {:ok, output} <- evaluate_template(d, "evaleex", content, d.data), do: String.trim(output) end ############################################################################## @doc """ TODO FOR path and canonical_path: * figure out how to handle markdown suffix conversion intelligently * peek into target file and see if it has layout/folder in @spec and drop index.html respectively """ def canonical_path() do with {:ok, root} <- Dragon.get(:root), %{origin: origin} = head <- Dragon.frame_head() do path = drop_root(root, origin, absolute: true) path = if String.ends_with?(path, ".md"), do: String.slice(path, 0..-4) <> ".html", else: path if String.ends_with?(path, "index.html") do String.slice(path, 0..-11) <> "/" else path end |> file_is_folder(head) end |> one_slash() end defp file_is_folder(path, %{page: %{"@spec": %{output: "folder/index"}}}) do if String.ends_with?(path, ".html") do String.slice(path, 0..-6) <> "/" else path end end defp file_is_folder(path, _head), do: path ############################################################################## def is_url(path), do: Regex.match?(~r/^([a-z]+):\/\//, path) def path("#" <> _id = fragment), do: fragment def path(dest) do # don't change URLs, only paths if is_url(dest) do dest else with {:ok, path, root} <- fix_path(dest), {:ok, build} <- Dragon.get(:build) do build_target = (Path.split(build) ++ (drop_root(root, path, absolute: false) |> Path.split())) |> Path.join() ## TODO: create a post-process work queue of lambdas, and put this check there if not path_exists?(build_target), do: warn(" #{path} (#{build_target}) is not valid") path = "/#{Path.split(path) |> Enum.join("/")}" |> one_slash() case Path.extname(path) do "" -> path <> "/" _ -> path end end end end def canonical_url(url, dest) do if is_url(dest), do: dest, else: url <> dest end # try directories and files; not very precise, but :shrug: defp path_exists?(target), do: exists_as_file?(target) or exists_as_indexed_folder?(target) or exists_as_folder?(target) defp exists_as_file?(target), do: File.exists?(target) defp exists_as_indexed_folder?(target), do: String.ends_with?(target, "index.html") and File.dir?(String.slice(target, 0..-11)) # technically we should peek into the file's headers to see if it has folder/index, but for now just guess defp exists_as_folder?(target), do: String.ends_with?(target, ".html") and File.dir?(String.slice(target, 0..-6)) defp one_slash(str), do: Regex.replace(~r|//+|, str, "/") ############################################################################## def get_header(path) do with {:ok, path, root} <- fix_path(path), {:ok, path} <- find_file(root, path), {:ok, header, _, _, _} <- Dragon.Template.Read.read_template_header(path), do: Dragon.Data.clean_data(header) end ############################################################################## def get_data(path, opts \\ []) do with {:ok, path, _root} <- fix_path(path), {:ok, dragon} <- Dragon.get(), %Dragon{} = d <- Dragon.Data.File.load(%Dragon{dragon | data: %{}}, %{type: "file", path: path}) do data = Transmogrify.transmogrify(d.data) if opts[:pop] do Enum.reduce_while(1..opts[:pop], data, fn _, data -> case Map.keys(data) do [key] -> {:cont, data[key]} _ -> {:halt, data} end end) else data end end end def get_with_key(a, b) do case Map.get(a, Transmogrify.As.as_key(b)) do nil -> raise Dragon.AbortError, "Key '#{b}' not found in: #{inspect(a)}" result -> result end end ############################################################################## # move to Tools.File defp fix_path(path) do path = Path.join(Path.split(path)) with {:ok, root} <- Dragon.get(:root), {:ok, p} <- drop_root(root, path, absolute: true) |> fix_relative_path(root) do {:ok, p, root} end end defp fix_relative_path("/" <> path, _), do: {:ok, path} defp fix_relative_path(path, root) do with %{this: parent} <- Dragon.frame_head() do {:ok, drop_root(root, parent, absolute: false) |> Path.dirname() |> Path.join(path)} end end ############################################################################## # note: Calendar.strftime %z doesn't work for ISO time, because %z returns # [+-]HHMM, and ISO time wants [+-]HH:MM. Unfortunately Calendar.strftime # has no option for the latter, and looks to be a dead projects, with stale # PRs untouched for years. # this assumes your times are always coming in as UTC, which may not be true. @isotime "%Y-%m-%dT%H:%M:%SZ" def date(d, fmt \\ @isotime) def date(d, fmt) when is_binary(d) do case DateTime.from_iso8601(d) do {:ok, d, _} -> date(d, fmt) {:error, what} -> raise Dragon.AbortError, message: "Unrecognized date string (not ISO 8601) #{d}: #{what}" end end def date(d, fmt) do with {:ok, date} <- Calendar.strftime(d, fmt) do date end end ############################################################################## def datefrom(f, fmt \\ @isotime) def datefrom(file, fmt) when is_binary(file), do: datefrom([file], fmt) def datefrom(files, fmt) when is_list(files) do files |> Enum.map(fn p -> with {:ok, path, root} <- fix_path(p), {:ok, stat} <- File.stat(Path.join(root, path), time: :posix) do stat.mtime else {:error, :enoent} -> raise Dragon.AbortError, message: "File not found: #{p}" {:error, err} -> raise Dragon.AbortError, message: to_string(err) end end) |> Enum.max() |> DateTime.from_unix!(:second) |> date(fmt) end ############################################################################## def in_collection(key) do key = "#{key}_index" |> String.to_atom() %{origin: origin} = Dragon.frame_head() fkey = Dragon.Data.Collection.collection_key(origin) Dragon.get!(:data)[key][fkey] end end