defmodule Serum.PostList do
@moduledoc """
Defines a struct representing a list of blog posts.
## Fields
* `tag`: Specifies by which tag the posts are filtered. Can be `nil`
* `current_page`: Number of current page
* `max_page`: Number of the last page
* `title`: Title of the list
* `posts`: A list of `Post` structs
* `url`: Absolute URL of this list page in the website
* `prev_url`: Absolute URL of the previous list page. Can be `nil` if this is
the first page
* `next_url`: Absolute URL of the next list page. Can be `nil` if this is
the last page
* `output`: Destination path
"""
alias Serum.Fragment
alias Serum.Plugin
alias Serum.Project
alias Serum.Renderer
alias Serum.Result
alias Serum.Tag
alias Serum.Template.Storage, as: TS
@type t :: %__MODULE__{
tag: maybe_tag(),
current_page: pos_integer(),
max_page: pos_integer(),
title: binary(),
posts: [map()],
url: binary(),
prev_url: binary() | nil,
next_url: binary() | nil,
output: binary(),
extras: %{optional(binary()) => binary()}
}
@type maybe_tag :: Tag.t() | nil
defstruct [
:tag,
:current_page,
:max_page,
:title,
:posts,
:url,
:prev_url,
:next_url,
:output,
:extras
]
@spec generate(maybe_tag(), [map()], Project.t()) :: Result.t([t()])
def generate(tag, posts, %Project{} = proj) do
paginate? = proj.pagination
num_posts = proj.posts_per_page
paginated_posts =
posts
|> make_chunks(paginate?, num_posts)
|> Enum.with_index(1)
max_page = length(paginated_posts)
list_dir = (tag && Path.join(proj.tags_path, tag.name)) || proj.posts_path
lists =
Enum.map(paginated_posts, fn {posts, page} ->
supplemental =
if page == 1,
do: [],
else: [prev: Path.join([proj.base_url, list_dir, format_name(page - 1)])]
supplemental =
if page == max_page,
do: supplemental,
else:
Keyword.put(
supplemental,
:next,
Path.join([proj.base_url, list_dir, format_name(page + 1)])
)
%__MODULE__{
tag: tag,
current_page: page,
max_page: max_page,
title: list_title(tag, proj),
posts: posts,
url: Path.join([proj.base_url, list_dir, format_name(page)]),
output: Path.join([proj.dest, list_dir, format_name(page)]),
extras: %{supplemental: supplemental}
}
end)
[first | rest] = put_adjacent_urls([nil | lists], [])
first_dup = %__MODULE__{
first
| url: Path.join([proj.base_url, list_dir, "index.html"]),
output: Path.join([proj.dest, list_dir, "index.html"])
}
[first_dup, first | rest]
|> Enum.map(&Plugin.processed_list/1)
|> Result.aggregate_values(:generate_lists)
end
@spec format_name(pos_integer(), String.t(), String.t()) :: String.t()
defp format_name(number, prefix \\ "page-", suffix \\ ".html"),
do: "#{prefix}#{number}#{suffix}"
@spec compact(t()) :: map()
def compact(%__MODULE__{} = list) do
list
|> Map.drop(~w(__struct__ output)a)
|> Map.put(:type, :list)
end
@spec put_adjacent_urls([nil | t()], [t()]) :: [t()]
defp put_adjacent_urls(lists, acc)
defp put_adjacent_urls([_last], acc), do: Enum.reverse(acc)
defp put_adjacent_urls([prev, curr | rest], acc) do
next = List.first(rest)
updated_curr = %__MODULE__{
curr
| prev_url: prev && prev.url,
next_url: next && next.url
}
put_adjacent_urls([curr | rest], [updated_curr | acc])
end
@spec make_chunks([map()], boolean(), pos_integer()) :: [[map()]]
defp make_chunks(posts, paginate?, num_posts)
defp make_chunks([], _, _), do: [[]]
defp make_chunks(posts, false, _), do: [posts]
defp make_chunks(posts, true, num_posts) do
Enum.chunk_every(posts, num_posts)
end
@spec list_title(maybe_tag(), Project.t()) :: binary()
defp list_title(tag, proj)
defp list_title(nil, proj), do: proj.list_title_all
defp list_title(%Tag{name: tag_name}, proj) do
proj.list_title_tag
|> :io_lib.format([tag_name])
|> IO.iodata_to_binary()
end
@spec to_fragment(t()) :: Result.t(Fragment.t())
def to_fragment(post_list) do
metadata = compact(post_list)
template = TS.get("list", :template)
bindings = [page: metadata]
case Renderer.render_fragment(template, bindings) do
{:ok, html} -> Fragment.new(nil, post_list.output, metadata, html)
{:error, _} = error -> error
end
end
defimpl Fragment.Source do
alias Serum.PostList
alias Serum.Result
@spec to_fragment(PostList.t()) :: Result.t(Fragment.t())
def to_fragment(fragment) do
PostList.to_fragment(fragment)
end
end
end