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