Packages
fermo
0.11.0
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.1
0.17.0
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.0
0.14.9
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.4
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A static site generator
Current section
Files
Jump to
Current section
Files
lib/fermo.ex
defmodule Fermo do
require EEx
require Slime
import Fermo.Naming
import Mix.Fermo.Paths
@source_path "priv/source"
def start(_start_type, _args \\ []) do
{:ok} = FermoHelpers.start_link([:assets, :i18n])
{:ok, self()}
end
@doc false
defmacro __using__(opts \\ %{}) do
quote do
require Fermo
@before_compile Fermo
Module.register_attribute __MODULE__, :config, persist: true
@config unquote(opts)
import FermoHelpers.Assets
import FermoHelpers.DateTime
import FermoHelpers.I18n
import FermoHelpers.Links
import FermoHelpers.String
import FermoHelpers.Text
def environment, do: "production" # TODO
end
end
@doc false
defmacro __before_compile__(_env) do
quote do
def initial_config() do
config = hd(__MODULE__.__info__(:attributes)[:config])
build_path = config[:build_path] || "build"
pages = config[:pages] || []
statics = config[:statics] || []
config
|> put_in([:build_path], build_path)
|> put_in([:pages], pages)
|> put_in([:statics], statics)
|> Fermo.Localizable.add()
|> Fermo.Simple.add()
|> put_in([:stats], %{})
|> put_in([:stats, :start], Time.utc_now)
end
end
end
def page(config, template, target, params \\ nil, options \\ nil) do
Fermo.add_page(config, template, target, params, options)
end
def paginate(config, template, options \\ %{}, context \\ %{}, fun \\ nil) do
Fermo.Pagination.paginate(config, template, options, context, fun)
end
def template_to_target(template, opts) do
String.replace(template, ".slim", "")
|> path_to_target(opts)
end
def path_to_target(path, opts \\ [])
def path_to_target(path, as_index_html: false), do: path
def path_to_target(path, _opts) do
cond do
path == "index.html" -> path
String.ends_with?(path, "/index.html") -> path
String.ends_with?(path, ".html") ->
String.replace(path, ".html", "/index.html")
true ->
path <> "/index.html"
end
end
def target_to_path(target) do
cond do
target == "index.html" -> "/"
String.ends_with?(target, "/index.html") ->
String.replace(target, "index.html", "")
true -> target
end
end
def add_page(config, template, target, params \\ %{}, options \\ %{}) do
pages = Map.get(config, :pages, [])
page = page_from(template, target, params, options)
put_in(config, [:pages], pages ++ [page])
end
def add_static(config, source, target) do
statics = Map.get(config, :statics)
put_in(config, [:statics], statics ++ [%{source: source, target: target}])
end
def page_from(template, target, params \\ %{}, options \\ %{}) do
%{
template: template,
target: target,
params: params,
options: options
}
end
def build(config) do
config = put_in(config, [:stats, :build_started], Time.utc_now)
{:ok} = FermoHelpers.build_assets()
{:ok} = FermoHelpers.load_i18n()
build_path = get_in(config, [:build_path])
File.mkdir(build_path)
config =
config
|> copy_statics()
|> set_paths()
|> Fermo.Sitemap.build()
|> merge_default_options()
|> Fermo.I18n.optionally_build_path_map()
|> build_pages()
{:ok, config}
end
defp copy_statics(config) do
statics = config[:statics]
build_path = get_in(config, [:build_path])
Enum.each(statics, fn (%{source: source, target: target}) ->
source_pathname = Path.join(@source_path, source)
target_pathname = Path.join(build_path, target)
copy_file(source_pathname, target_pathname)
end)
put_in(config, [:stats, :copy_statics_completed], Time.utc_now)
end
defp set_paths(config) do
build_path = get_in(config, [:build_path])
pages = Enum.map(
config.pages,
fn %{template: template, target: supplied_target} = page ->
module = module_for_template(template)
context = build_context(module, template, page)
params = params_for(module, page)
path_override = apply(module, :content_for, [:path, params, context])
# This depends on the default content_for returning "" and not nil
[target, path] = if path_override == "" do
[supplied_target, target_to_path(supplied_target)]
else
# Avoid extra whitespace introduced by templating
path = String.replace(path_override, ~r/\n/, "")
[path_to_target(path, as_index_html: true), path]
end
pathname = Path.join(build_path, target)
page
|> put_in([:target], target)
|> put_in([:path], path)
|> put_in([:pathname], pathname)
end
)
config
|> put_in([:pages], pages)
|> put_in([:stats, :set_paths_completed], Time.utc_now)
end
defp merge_default_options(config) do
pages = Enum.map(
config.pages,
fn %{template: template} = page ->
module = module_for_template(template)
defaults = defaults_for(module)
layout = if Map.has_key?(defaults, "layout") do
if defaults["layout"] do
defaults["layout"] <> ".html.slim"
else
defaults["layout"]
end
else
if Map.has_key?(config, :layout) do
config.layout
else
"layouts/layout.html.slim"
end
end
options =
defaults
|> Map.merge(page.options)
|> put_in([:module], module)
|> put_in([:layout], layout)
put_in(page, [:options], options)
end
)
config
|> put_in([:pages], pages)
|> put_in([:stats, :merge_default_options_completed], Time.utc_now)
end
defp build_pages(config) do
# TODO: check if Webpack assets are ready before building HTML
# TODO: avoid passing config into tasks - decide the layout beforehand
Task.async_stream(
config.pages,
&(render_page(&1)),
[timeout: :infinity]
) |> Enum.to_list
config
|> put_in([:stats, :build_pages_completed], Time.utc_now)
end
defp render_page(page) do
with {:ok, hash} <- cache_key(page),
{:ok, cache_pathname} <- cached_page_path(hash),
{:ok} <- is_cached?(cache_pathname) do
copy_file(cache_pathname, page.pathname)
else
{:build_and_cache, cache_pathname} ->
body = inner_render_page(page)
save_file(cache_pathname, body)
save_file(page.pathname, body)
_ ->
body = inner_render_page(page)
save_file(page.pathname, body)
end
end
defp cache_key(%{options: %{surrogate_key: surrogate_key}}) do
hash = :crypto.hash(:sha256, surrogate_key) |> Base.encode16
{:ok, hash}
end
defp cache_key(_page), do: {:no_key}
defp cached_page_path(hash) do
{:ok, Path.join("tmp/page_cache", hash)}
end
defp is_cached?(cached_pathname) do
if File.exists?(cached_pathname) do
{:ok}
else
{:build_and_cache, cached_pathname}
end
end
defp inner_render_page(page) do
content = render_body(page.options.module, page)
if page.options.layout do
build_layout_with_content(page.options.layout, content, page)
else
content
end
end
defp copy_file(source, destination) do
path = Path.dirname(destination)
File.mkdir_p(path)
{:ok, _files} = File.cp_r(source, destination)
end
defp save_file(pathname, body) do
path = Path.dirname(pathname)
File.mkdir_p(path)
File.write!(pathname, body, [:write])
end
defp defaults_for(module) do
apply(module, :defaults, [])
|> Enum.into(
%{},
fn {key, value} ->
{String.to_atom(key), value}
end
)
end
defp params_for(module, page) do
defaults = defaults_for(module)
Map.merge(defaults, page.params)
end
defp render_body(module, %{template: template} = page) do
params = params_for(module, page)
render_template(module, template, page, params)
end
def render_template(module, template, page, params \\ %{}) do
context = build_context(module, template, page)
apply(module, :call, [params, context])
end
defp build_context(module, template, page) do
env = System.get_env()
%{
module: module,
template: template,
page: page,
env: env
}
end
defp build_layout_with_content(layout, content, page) do
module = module_for_template(layout)
layout_params = Map.merge(page.params, %{content: content})
render_template(module, layout, page, layout_params)
end
def full_template_path(path) do
Path.join(@source_path, path)
end
def module_for_template(template) do
template
|> absolute_to_source()
|> source_path_to_module()
end
end