Packages

High performant static blog generator.

Current section

Files

Jump to
reblog lib engine.ex
Raw

lib/engine.ex

defmodule Reblog.Engine do
@moduledoc """
Reblog blog engine to build content.
"""
require List
require Reblog.Engine.VinylFile
require EEx
require Logger
def build(src_in, out) do
before = System.monotonic_time()
File.cd!(src_in)
name = Recase.to_pascal(Path.basename(System.cwd()))
{{year, month, day}, _} = :calendar.universal_time()
Logger.info(fn -> [IO.ANSI.blue, IO.ANSI.underline, "Building #{name}", IO.ANSI.reset] end)
Logger.info(fn -> [" source: ", src_in] end)
Logger.info(fn -> [" out: ", out] end)
src_from_out = String.duplicate("../", src_in
|> Path.relative_to(out)
|> Path.split()
|> Enum.count())
context = %{src: src_from_out,
name: name,
out: out,
page_size: 5,
generated_time: "#{month}/#{day}/#{year}",
template_layout: ".layout.eex",
pages: "pages",
posts: "posts",
assets: "assets",
blog: "blog"}
if File.exists?(context.out), do: File.rm_rf!(context.out)
File.mkdir!(context.out)
Logger.info(fn -> [IO.ANSI.light_magenta, "Building maps...", IO.ANSI.reset] end)
context = build_mapping(context)
Logger.info(fn -> [IO.ANSI.light_magenta, "Loading layouts...", IO.ANSI.reset] end)
context = Reblog.Engine.Loader.load(:base_layout, context)
Logger.info(fn -> [IO.ANSI.light_magenta, "Building pages...", IO.ANSI.reset] end)
context = build_pages(context)
Logger.info(fn -> [IO.ANSI.light_magenta, "Building posts...", IO.ANSI.reset] end)
context = build_posts(context)
Logger.info(fn -> [IO.ANSI.light_magenta, "Building pagination...", IO.ANSI.reset] end)
context = build_pagination(context)
Logger.info(fn -> [IO.ANSI.light_magenta, "Building assets...", IO.ANSI.reset] end)
context = build_assets(context)
Logger.info(fn -> [IO.ANSI.light_magenta, "Cleaning...", IO.ANSI.reset] end)
_context = copy_first_page(context)
later = System.monotonic_time()
seconds =
Float.round(System.convert_time_unit(later - before, :native, :milliseconds) / 1000, 3)
Logger.metadata(namespace: nil)
Logger.info(fn ->
[IO.ANSI.green(), "Built successfully to '#{out}'. <#{seconds}s>", IO.ANSI.reset()]
end)
end
defp build_mapping(context) do
out_mapping = Path.join(context.out, ".map.yml")
Logger.info(fn -> [IO.ANSI.cyan, " #{out_mapping}", IO.ANSI.reset] end)
File.write!(out_mapping, """
src: #{context.src}
name: #{context.name}
out: #{context.out}
page_size: #{context.page_size}
generated_time: #{context.generated_time}
template_layout: #{context.template_layout}
pages: #{context.pages}
posts: #{context.posts}
assets: #{context.assets}
blog: #{context.blog}
""")
context
end
defp build_pages(context) do
pages = File.ls!(context.pages)
pages
|> Stream.filter(fn file ->
not String.starts_with?(Path.basename(file), ".")
end)
|> Enum.map(fn file ->
path = Path.join(context.pages, file)
Logger.info(fn -> [IO.ANSI.cyan, " page: #{path}", IO.ANSI.reset] end)
Task.async(fn ->
vinyl = Reblog.Engine.VinylFile.new(path)
{rendered, context} = Reblog.Engine.Builder.build(:page, vinyl, context)
File.mkdir!(Path.join(context.out, "#{vinyl.stem}"))
File.write!(Path.join(context.out, "#{vinyl.stem}/index.html"), rendered)
context
end)
end)
|> Enum.each(&Task.await/1)
context
end
defp build_posts(context) do
File.mkdir!(Path.join(context.out, context.blog))
posts = File.ls!(context.posts)
processed_posts = posts
|> Stream.filter(fn file ->
not String.starts_with?(Path.basename(file), ".")
end)
|> Enum.sort()
|> Enum.reverse()
|> Enum.map(fn file ->
path = Path.join(context.posts, file)
Logger.info(fn -> [IO.ANSI.cyan, " post: #{path}", IO.ANSI.reset] end)
Task.async(fn ->
vinyl = Reblog.Engine.VinylFile.new(path)
{rendered, context} = Reblog.Engine.Builder.build(:post, vinyl, context)
File.write!(Path.join([context.out, context.blog, "#{vinyl.stem}.html"]), rendered)
context
end)
end)
|> Enum.map(&Task.await/1)
Logger.debug(fn -> "Processed Posts: #{inspect processed_posts}" end)
Map.put(context, :processed_posts, processed_posts)
end
defp build_pagination(context) do
File.mkdir!(Path.join([context.out, context.blog, context.pages]))
pagination_layout = File.read!(Path.join(context.posts, context.template_layout))
pages = Enum.chunk_every(context.processed_posts, context.page_size)
total_pages = length(pages)
0..(total_pages - 1)
|> Enum.map(fn i ->
Logger.info(fn -> [IO.ANSI.cyan, " page: #{i + 1}/#{total_pages}", IO.ANSI.reset] end)
previous_page_number = if i - 1 >= 0, do: i - 1, else: nil
next_page_number = if i + 1 < total_pages, do: i + 1, else: nil
previous_page_path =
if previous_page_number != nil,
do: Path.join([context.blog, "pages/#{previous_page_number}"]),
else: nil
next_page_path =
if next_page_number != nil,
do: Path.join([context.blog, "pages/#{next_page_number}"]),
else: nil
Task.async(fn ->
eex_context = %{
title: context.name,
body: pagination_layout,
current_page: i,
total_pages: total_pages,
previous_page_number: previous_page_number,
previous_page_path: previous_page_path,
next_page_number: next_page_number,
next_page_path: next_page_path,
pagination_posts: Enum.at(pages, i)
}
eex_context = Map.merge(context, eex_context)
pagination_rendered = EEx.eval_string(eex_context.base_layout, assigns: Map.to_list(eex_context))
pagination_rendered = EEx.eval_string(pagination_rendered, assigns: Map.to_list(eex_context))
out_page = Path.join([eex_context.out, eex_context.blog, "pages/#{i}"])
File.mkdir!(out_page)
File.write!(Path.join(out_page, "index.html"), pagination_rendered)
eex_context
end)
end)
|> Enum.each(&Task.await/1)
context
end
defp build_assets(context) do
out_assets = Path.join(context.out, context.assets)
File.mkdir(out_assets)
File.cp_r!(context.assets, out_assets)
context
end
defp copy_first_page(context) do
out_first_page = Path.join([context.out, context.blog, "pages/0/index.html"])
if File.exists?(out_first_page) do
File.cp_r!(out_first_page, Path.join(context.out, "index.html"))
end
context
end
end