defmodule Serum.Post do @moduledoc """ Defines a struct representing a blog post page. ## Fields * `file`: Source path * `title`: Post title * `date`: Post date (formatted) * `raw_date`: Post date (erlang tuple style) * `tags`: A list of tags * `url`: Absolute URL of the blog post in the website * `canonical_url`: Custom canonical URL of the blog post * `html`: Post contents converted into HTML * `preview`: Preview text of the post * `output`: Destination path """ alias Serum.Fragment alias Serum.Post.PreviewGenerator alias Serum.Project alias Serum.Renderer alias Serum.Result alias Serum.Tag alias Serum.Template alias Serum.Template.Storage, as: TS @type t :: %__MODULE__{ file: binary(), title: binary(), date: binary(), raw_date: :calendar.datetime(), tags: [Tag.t()], url: binary(), canonical_url: binary(), html: binary(), preview: binary(), output: binary(), extras: map(), template: binary() | nil } defstruct [ :file, :title, :date, :raw_date, :tags, :url, :canonical_url, :html, :preview, :output, :extras, :template ] @spec new(binary(), {map(), map()}, binary(), Project.t()) :: t() def new(path, {header, extras}, html, %Project{} = proj) do tags = Tag.batch_create(header[:tags] || [], proj) datetime = header[:date] || Date.utc_today() date_str = Timex.format!(datetime, proj.date_format) raw_date = to_erl_datetime(datetime) preview = PreviewGenerator.generate_preview(html, proj.preview_length) {url, output} = path |> Path.basename(".md") |> url_and_output(proj) %__MODULE__{ file: path, title: header[:title], tags: tags, html: html, preview: preview, raw_date: raw_date, date: date_str, url: url, canonical_url: header[:canonical_url], output: output, template: header[:template], extras: extras } end @spec compact(t()) :: map() def compact(%__MODULE__{} = post) do post |> Map.drop(~w(__struct__ file html output)a) |> Map.put(:type, :post) end @spec to_erl_datetime(term()) :: :calendar.datetime() defp to_erl_datetime(obj) do case Timex.to_erl(obj) do {{_y, _m, _d}, {_h, _i, _s}} = erl_datetime -> erl_datetime {_y, _m, _d} = erl_date -> {erl_date, {0, 0, 0}} _ -> {{0, 1, 1}, {0, 0, 0}} end end @spec url_and_output(binary(), Project.t()) :: {binary(), binary()} defp url_and_output(basename, proj) do if proj.pretty_urls in [true, :posts] do { Path.join([proj.base_url, proj.posts_path, basename]), Path.join([proj.dest, proj.posts_path, basename, "index.html"]) } else { Path.join([proj.base_url, proj.posts_path, basename <> ".html"]), Path.join([proj.dest, proj.posts_path, basename <> ".html"]) } end end @spec to_fragment(t()) :: Result.t(Fragment.t()) def to_fragment(post) do metadata = compact(post) template_name = post.template || "post" bindings = [page: metadata, contents: post.html] with %Template{} = template <- TS.get(template_name, :template), {:ok, html} <- Renderer.render_fragment(template, bindings) do Fragment.new(post.file, post.output, metadata, html) else nil -> {:error, "the template \"#{template_name}\" is not available"} {:error, _} = error -> error end end defimpl Fragment.Source do alias Serum.Post alias Serum.Result @spec to_fragment(Post.t()) :: Result.t(Fragment.t()) def to_fragment(post) do Post.to_fragment(post) end end end