Packages

Yet another static website generator written in Elixir with MD parser (forked from serum)

Current section

Files

Jump to
serum_md lib serum build.ex
Raw

lib/serum/build.ex

defmodule Serum.Build do
@moduledoc """
A module for managing the overall project build procedure.
"""
import Serum.IOProxy, only: [put_msg: 2]
alias Serum.Build.FileCopier
alias Serum.Build.FileEmitter
alias Serum.Build.FileLoader
alias Serum.Build.FileProcessor
alias Serum.Build.FragmentGenerator
alias Serum.Build.PageGenerator
alias Serum.Plugin
alias Serum.Project
alias Serum.Result
alias Serum.Theme
@doc """
Builds the given Serum project.
## Build Procedure
1. Tries to load plugins and a theme.
2. Checks if the system timezone is properly set.
Timex requires the local timezone information to format the date/time
string. If it's not set or invalid, Timex will fail.
3. Checks if the current user has enough permission on the destination
directory and cleans it if it already exists.
4. Loads source files. See `Serum.Build.FileLoader`.
5. Processes source files and produces intermediate data structures.
See `Serum.Build.FileProcessor`.
6. Generates HTML fragments from the intermediate data.
See `Serum.Build.FragmentGenerator`.
7. Renders full HTML pages from fragments and writes them to files.
See `Serum.Build.FileEmitter`.
8. Copies `assets/` and `media/` directories if they exist.
"""
@spec build(Project.t()) :: Result.t(binary())
def build(%Project{src: src, dest: dest} = proj) do
with {:ok, proj} <- load_plugins(proj),
:ok <- Plugin.build_started(src, dest),
:ok <- pre_check(dest),
:ok <- do_build(proj),
:ok <- Plugin.build_succeeded(src, dest),
:ok <- Plugin.finalizing(src, dest) do
{:ok, dest}
else
{:error, _} = error ->
with :ok <- Plugin.build_failed(src, dest, error),
:ok <- Plugin.finalizing(src, dest) do
error
else
{:error, _} = plugin_error -> plugin_error
end
end
end
@spec fix_url(binary() | Serum.Page.t() | Serum.File.t()) :: String.t()
def fix_url(url) when is_binary(url), do: url
def fix_url(%Serum.Page{url: url}) when is_binary(url), do: url
def fix_url(%Serum.File{dest: dest}) when is_binary(dest), do: dest
def fix_url(%Serum.File{src: src}) when is_binary(src),
do: src |> Path.split() |> then(&[List.first(&1), List.last(&1)]) |> Path.join()
def fix_url(_), do: nil
@spec load_plugins(Project.t()) :: Result.t(Project.t())
defp load_plugins(proj) do
with {:ok, plugins} <- Plugin.load_plugins(proj.plugins),
{:ok, theme} <- Theme.load(proj.theme.module) do
Plugin.show_info(plugins)
{:ok, %Project{proj | theme: theme}}
else
{:error, _} = error -> error
end
end
@spec pre_check(binary()) :: Result.t()
defp pre_check(dest) do
with :ok <- check_tz(),
:ok <- check_dest_perm(dest) do
clean_dest(dest)
else
{:error, _} = error -> error
end
end
@spec do_build(Project.t()) :: Result.t()
defp do_build(%{src: src, dest: dest} = proj) do
with {:ok, files} <- FileLoader.load_files(proj),
{:ok, map} <- FileProcessor.process_files(files, proj),
{:ok, fragments} <- FragmentGenerator.to_fragment(map),
{:ok, files} <- PageGenerator.run(fragments),
:ok <- FileEmitter.run(files) do
FileCopier.copy_files(src, dest)
else
{:error, _} = error -> error
end
end
# Checks if the system timezone is set and valid.
@spec check_tz() :: Result.t()
defp check_tz do
Timex.local()
:ok
rescue
_ -> {:error, "system timezone is not set"}
end
# Checks if the effective user have a write
# permission on the destination directory.
@spec check_dest_perm(binary) :: Result.t()
defp check_dest_perm(dest) do
parent = dest |> Path.join("") |> Path.dirname()
result =
case File.stat(parent) do
{:error, reason} -> reason
{:ok, %File.Stat{access: :none}} -> :eacces
{:ok, %File.Stat{access: :read}} -> :eacces
{:ok, _} -> :ok
end
case result do
:ok -> :ok
err -> {:error, {err, dest, 0}}
end
end
# Removes all files and directories in the destination directory,
# excluding dotfiles so that git repository is not blown away.
@spec clean_dest(binary) :: Result.t()
defp clean_dest(dest) do
File.mkdir_p!(dest)
put_msg(:mkdir, dest)
dest
|> File.ls!()
|> Enum.reject(&String.starts_with?(&1, "."))
|> Enum.map(&Path.join(dest, &1))
|> Enum.map(fn path ->
case File.rm_rf(path) do
{:ok, _} -> :ok
{:error, reason, ^path} -> {:error, {reason, path, 0}}
end
end)
|> Result.aggregate(:clean_dest)
end
end