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