chore: another god module down

This commit is contained in:
2026-05-01 12:11:15 +02:00
parent 3f985885a4
commit 52857f2959
7 changed files with 875 additions and 816 deletions

View File

@@ -1,838 +1,33 @@
defmodule BDS.Rendering do
@moduledoc false
import Ecto.Query
alias BDS.Frontmatter
alias BDS.Persistence
alias BDS.Media.Media, as: MediaAsset
alias BDS.Menu
alias BDS.Metadata
alias BDS.PreviewAssets
alias BDS.PostLinks
alias BDS.Projects
alias BDS.I18n
alias BDS.Rendering.FileSystem
alias BDS.Rendering.Filters
alias BDS.Repo
alias BDS.StarterTemplates
alias BDS.Posts.Post
alias BDS.Posts.Translation
alias BDS.Tags.Tag
alias BDS.Templates.Template
alias BDS.Rendering.ListArchive
alias BDS.Rendering.PostRendering
alias BDS.Rendering.TemplateSelection
def render_post_page(project_id, template_slug, assigns)
when is_binary(project_id) and is_map(assigns) do
with {:ok, template_source} <- load_template_source(project_id, :post, template_slug),
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :post, template_slug),
{:ok, rendered} <-
render_template(project_id, template_source, post_assigns(project_id, assigns)) do
TemplateSelection.render_template(project_id, template_source, PostRendering.post_assigns(project_id, assigns)) do
{:ok, rendered}
end
end
def render_list_page(project_id, assigns) when is_binary(project_id) and is_map(assigns) do
with {:ok, template_source} <- load_template_source(project_id, :list, nil),
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :list, nil),
{:ok, rendered} <-
render_template(project_id, template_source, list_assigns(project_id, assigns)) do
TemplateSelection.render_template(project_id, template_source, ListArchive.list_assigns(project_id, assigns)) do
{:ok, rendered}
end
end
def render_not_found_page(project_id, assigns \\ %{})
when is_binary(project_id) and is_map(assigns) do
with {:ok, template_source} <- load_template_source(project_id, :not_found, nil),
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :not_found, nil),
{:ok, rendered} <-
render_template(project_id, template_source, not_found_assigns(project_id, assigns)) do
TemplateSelection.render_template(project_id, template_source, ListArchive.not_found_assigns(project_id, assigns)) do
{:ok, rendered}
end
end
defp load_template_source(project_id, kind, slug) do
project = Projects.get_project!(project_id)
case select_template(project_id, kind, slug) do
%Template{} = template ->
case published_template_body(template) do
{:ok, _source} = ok ->
ok
{:error, reason} = error ->
maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
end
nil ->
load_bundled_template_source(project, kind, slug)
end
end
defp select_template(project_id, kind, slug) when is_binary(slug) and slug != "" do
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == ^kind and
template.status == :published and
template.enabled == true and template.slug == ^slug,
limit: 1
)
end
defp select_template(project_id, :post, nil) do
case StarterTemplates.default_slug(:post) do
nil ->
nil
default_slug ->
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == :post and
template.status == :published and
template.enabled == true and template.slug == ^default_slug,
limit: 1
)
end
end
defp select_template(project_id, kind, nil) do
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == ^kind and
template.status == :published and
template.enabled == true,
order_by: [desc: template.created_at, desc: template.slug],
limit: 1
)
end
defp published_template_body(%Template{content: content}) when is_binary(content),
do: {:ok, content}
defp published_template_body(%Template{} = template) do
project = Projects.get_project!(template.project_id)
full_path = Path.join(Projects.project_data_dir(project), template.file_path)
case File.read(full_path) do
{:ok, contents} ->
case Frontmatter.parse_document(contents) do
{:ok, %{body: body}} -> {:ok, body}
{:error, reason} -> {:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
defp render_template(project_id, source, assigns) do
with {:ok, template_ast} <- Liquex.parse(source) do
project = Projects.get_project!(project_id)
context =
Liquex.Context.new(assigns,
static_environment: assigns,
filter_module: Filters,
file_system: FileSystem.new(StarterTemplates.template_roots(project))
)
{result, _context} = Liquex.render!(template_ast, context)
{:ok, IO.iodata_to_binary(result)}
end
rescue
error -> {:error, error}
end
defp load_bundled_template_source(project, kind, slug) do
desired_slug = bundled_template_slug(kind, slug)
if is_binary(desired_slug) do
file_system = project |> StarterTemplates.template_roots() |> FileSystem.new()
source = Liquex.FileSystem.read_template_file(file_system, desired_slug)
case Frontmatter.parse_document(source) do
{:ok, %{body: body}} -> {:ok, body}
{:error, :invalid_frontmatter} -> {:ok, source}
end
else
{:error, :template_not_found}
end
rescue
error in [Liquex.Error] ->
_ = error
{:error, :template_not_found}
end
defp maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
when reason in [:enoent, :template_not_found] do
if template.content in [nil, ""] and StarterTemplates.default_template?(kind, template.slug) do
load_bundled_template_source(project, kind, slug)
else
error
end
end
defp maybe_load_bundled_template_source(_project, _kind, _slug, _template, _reason, error),
do: error
defp bundled_template_slug(_kind, slug) when is_binary(slug) and slug != "", do: slug
defp bundled_template_slug(kind, _slug), do: StarterTemplates.default_slug(kind)
defp post_assigns(project_id, assigns) do
metadata = project_metadata(project_id)
template_context = template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
post_record = load_post_record(assigns)
canonical_post = canonical_post_record(post_record)
post_id = canonical_post_id(post_record, assigns)
post_categories = Map.get(post_record || %{}, :categories, []) || []
post_tags = Map.get(post_record || %{}, :tags, []) || []
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = canonical_media_path_by_source_path(project_id)
raw_content = Map.get(assigns, :content, Map.get(assigns, "content"))
rendered_content = render_post_content(raw_content, canonical_post_paths, canonical_media_paths, language, template_context)
incoming_links = link_contexts(project_id, post_id, :incoming, main_language)
outgoing_links = link_contexts(project_id, post_id, :outgoing, main_language)
post_assigns =
assigns
|> Map.put(:content, rendered_content)
|> Map.put(:raw_content, raw_content)
%{
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", language_prefix(language, main_language))
),
page_title:
Map.get(
assigns,
:page_title,
Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))
),
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", default_pico_stylesheet_href(metadata.pico_theme))
),
html_theme_attribute:
Map.get(
assigns,
:html_theme_attribute,
Map.get(assigns, "html_theme_attribute")
),
blog_languages: blog_languages(metadata, language),
alternate_links: alternate_links(canonical_post, project_id, main_language),
menu_items: menu_items(project_id),
calendar_initial_year: calendar_initial_year(post_record),
calendar_initial_month: calendar_initial_month(post_record),
post_categories: post_categories,
post_tags: post_tags,
tag_color_by_name: tag_color_by_name(project_id),
backlinks: backlinks(incoming_links),
canonical_post_path_by_slug: canonical_post_paths,
canonical_media_path_by_source_path: canonical_media_paths,
post_data_json_by_id: post_data_json(post_assigns, post_record),
post: build_post_context(post_assigns, post_record, incoming_links, outgoing_links)
}
end
defp list_assigns(project_id, assigns) do
metadata = project_metadata(project_id)
template_context = template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = canonical_media_path_by_source_path(project_id)
posts =
normalize_list_posts(
Map.get(assigns, :posts, Map.get(assigns, "posts", [])),
canonical_post_paths,
canonical_media_paths,
language,
template_context
)
pagination =
normalize_pagination(Map.get(assigns, :pagination, Map.get(assigns, "pagination")), posts)
day_blocks = build_day_blocks(posts)
min_date = min_date(posts)
max_date = max_date(posts)
normalized_archive_context = normalize_archive_context(archive_context)
%{
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", language_prefix(language, main_language))
),
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title")),
posts: posts,
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", default_pico_stylesheet_href(metadata.pico_theme))
),
html_theme_attribute:
Map.get(
assigns,
:html_theme_attribute,
Map.get(assigns, "html_theme_attribute")
),
blog_languages: blog_languages(metadata, language),
alternate_links: [],
menu_items: menu_items(project_id),
calendar_initial_year: calendar_initial_year_from_posts(posts),
calendar_initial_month: calendar_initial_month_from_posts(posts),
archive_context: normalized_archive_context,
show_archive_range_heading: show_archive_range_heading?(normalized_archive_context, day_blocks),
min_date: min_date,
max_date: max_date,
is_list_page: true,
is_first_page: pagination.current_page <= 1,
is_last_page: pagination.current_page >= pagination.total_pages,
has_prev_page: pagination.has_prev_page,
has_next_page: pagination.has_next_page,
prev_page_href: pagination.prev_page_href,
next_page_href: pagination.next_page_href,
current_page: pagination.current_page,
total_pages: pagination.total_pages,
total_items: pagination.total_items,
items_per_page: pagination.items_per_page,
canonical_post_path_by_slug: canonical_post_paths,
canonical_media_path_by_source_path: canonical_media_paths,
post_data_json_by_id:
Enum.into(posts, %{}, fn post -> {post.id, post_data_json_value(post)} end),
day_blocks: day_blocks
}
end
defp not_found_assigns(project_id, assigns) do
metadata = project_metadata(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
%{
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", "404")),
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", language_prefix(language, main_language))
),
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", default_pico_stylesheet_href(metadata.pico_theme))
),
html_theme_attribute:
Map.get(
assigns,
:html_theme_attribute,
Map.get(assigns, "html_theme_attribute")
),
blog_languages: blog_languages(metadata, language),
menu_items: menu_items(project_id),
alternate_links: [],
not_found_message:
Map.get(
assigns,
:not_found_message,
Map.get(
assigns,
"not_found_message",
I18n.translate(language, "render.notFound.message")
)
),
not_found_back_label:
Map.get(
assigns,
:not_found_back_label,
Map.get(
assigns,
"not_found_back_label",
I18n.translate(language, "render.notFound.back")
)
)
}
end
defp project_metadata(project_id) do
{:ok, metadata} = Metadata.get_project_metadata(project_id)
metadata
end
defp menu_items(project_id) do
{:ok, %{items: items}} = Menu.get_menu(project_id)
Enum.map(items, &to_template_menu_item/1)
end
defp to_template_menu_item(item) do
kind = Map.get(item, :kind)
children = Enum.map(Map.get(item, :children, []), &to_template_menu_item/1)
%{
title: Map.get(item, :label, ""),
href: menu_item_href(item),
has_children: children != [],
children: children,
kind: kind
}
end
defp menu_item_href(%{kind: :home}), do: "/"
defp menu_item_href(%{kind: :page, slug: slug}) when is_binary(slug) and slug != "",
do: "/#{slug}/"
defp menu_item_href(%{kind: :category_archive, slug: slug}) when is_binary(slug) and slug != "",
do: "/category/#{URI.encode(slug)}/"
defp menu_item_href(%{kind: :submenu}), do: "#"
defp menu_item_href(_item), do: "#"
defp blog_languages(metadata, current_language) do
([metadata.main_language] ++ (metadata.blog_languages || []))
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.uniq()
|> Enum.map(fn language ->
normalized = I18n.normalize_language(language)
href_prefix = language_prefix(normalized, metadata.main_language || current_language)
%{
code: normalized,
flag: I18n.flag(normalized),
href: href_for_language(href_prefix),
href_prefix: href_prefix,
is_current: normalized == I18n.normalize_language(current_language)
}
end)
end
defp tag_color_by_name(project_id) do
Repo.all(from tag in Tag, where: tag.project_id == ^project_id, select: {tag.name, tag.color})
|> Enum.into(%{}, fn {name, color} -> {name, color} end)
end
defp load_post_record(assigns) do
case Map.get(assigns, :id, Map.get(assigns, "id")) do
nil -> nil
post_id -> Repo.get(Post, post_id) || Repo.get(Translation, post_id)
end
end
defp canonical_post_record(%Translation{translation_for: post_id}), do: Repo.get(Post, post_id)
defp canonical_post_record(%Post{} = post), do: post
defp canonical_post_record(_other), do: nil
defp canonical_post_id(%Translation{translation_for: post_id}, _assigns), do: post_id
defp canonical_post_id(%Post{id: post_id}, _assigns), do: post_id
defp canonical_post_id(_post_record, assigns), do: Map.get(assigns, :id, Map.get(assigns, "id"))
defp post_data_json(assigns, post_record) do
id = Map.get(assigns, :id, Map.get(assigns, "id"))
if is_binary(id) do
incoming_links = link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :incoming, Map.get(post_record || %{}, :language))
outgoing_links = link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :outgoing, Map.get(post_record || %{}, :language))
%{
id => post_data_json_value(build_post_context(assigns, post_record, incoming_links, outgoing_links))
}
else
%{}
end
end
defp post_data_json_value(post_context) do
Jason.encode!(%{
id: Map.get(post_context, :id),
title: Map.get(post_context, :title),
slug: Map.get(post_context, :slug),
excerpt: Map.get(post_context, :excerpt),
author: Map.get(post_context, :author),
language: Map.get(post_context, :language),
published_at: Map.get(post_context, :published_at),
created_at: Map.get(post_context, :created_at),
updated_at: Map.get(post_context, :updated_at),
tags: Map.get(post_context, :tags, []),
categories: Map.get(post_context, :categories, [])
})
end
defp canonical_post_path_by_slug(project_id, main_language) do
posts =
Repo.all(
from post in Post, where: post.project_id == ^project_id and post.status == :published
)
translations =
Repo.all(
from translation in Translation,
where: translation.project_id == ^project_id and translation.status == :published
)
post_by_id = Map.new(posts, fn post -> {post.id, post} end)
post_paths =
Enum.into(posts, %{}, fn post ->
{post.slug, post_path(post, nil)}
end)
Enum.reduce(translations, post_paths, fn translation, acc ->
case Map.get(post_by_id, translation.translation_for) do
nil -> acc
post -> Map.put(acc, post.slug, post_path(post, translation.language, main_language))
end
end)
end
defp alternate_links(nil, _project_id, _main_language), do: []
defp alternate_links(%Post{} = post, project_id, main_language) do
translations =
Repo.all(
from translation in Translation,
where:
translation.project_id == ^project_id and
translation.translation_for == ^post.id and
translation.status == :published,
order_by: [asc: translation.language]
)
[%{href: post_path(post, nil), hreflang: normalize_language(post.language, main_language)}] ++
Enum.map(translations, fn translation ->
%{href: post_path(post, translation.language, main_language), hreflang: translation.language}
end)
end
defp backlinks(incoming_links) do
Enum.map(incoming_links, fn link ->
%{path: link.href, display_slug: link.display_slug, title: link.title}
end)
end
defp link_contexts(_project_id, nil, _direction, _main_language), do: []
defp link_contexts(project_id, post_id, :incoming, main_language) do
PostLinks.list_incoming_links(post_id)
|> Enum.map(&link_context(project_id, &1, :incoming, main_language))
|> Enum.reject(&is_nil/1)
end
defp link_contexts(project_id, post_id, :outgoing, main_language) do
PostLinks.list_outgoing_links(post_id)
|> Enum.map(&link_context(project_id, &1, :outgoing, main_language))
|> Enum.reject(&is_nil/1)
end
defp link_context(_project_id, link, direction, main_language) do
linked_post_id =
case direction do
:incoming -> link.source_post_id
:outgoing -> link.target_post_id
end
case Repo.get(Post, linked_post_id) do
nil -> nil
linked_post -> %{href: post_path(linked_post, nil), title: linked_post.title, display_slug: linked_post.slug, language: normalize_language(linked_post.language, main_language)}
end
end
defp canonical_media_path_by_source_path(project_id) do
Repo.all(from media in MediaAsset, where: media.project_id == ^project_id)
|> Enum.reduce(%{}, fn media, acc ->
datetime = Persistence.from_unix_ms!(media.created_at)
source_key =
Path.join([
"media",
Integer.to_string(datetime.year),
String.pad_leading(Integer.to_string(datetime.month), 2, "0"),
media.original_name
])
|> String.downcase()
Map.put(acc, source_key, "/" <> media.file_path)
end)
end
defp post_path(post, language_prefix)
when is_binary(language_prefix) and language_prefix != "" do
language_prefix <> post_path(post, nil)
end
defp post_path(post, ""), do: post_path(post, nil)
defp post_path(post, nil) do
datetime = Persistence.from_unix_ms!(post.created_at)
Path.join([
Integer.to_string(datetime.year),
String.pad_leading(Integer.to_string(datetime.month), 2, "0"),
String.pad_leading(Integer.to_string(datetime.day), 2, "0"),
post.slug,
"index.html"
])
|> then(&("/" <> String.trim_trailing(&1, "index.html")))
end
defp post_path(post, language, main_language) do
prefix = language_prefix(language, main_language)
post_path(post, prefix)
end
defp normalize_list_posts(posts, canonical_post_paths, canonical_media_paths, language, template_context) do
Enum.map(posts, fn post ->
post_record = load_post_record(post)
raw_content =
Map.get(
post,
:content,
Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
)
%{
id: Map.get(post, :id, Map.get(post, "id")),
slug: Map.get(post, :slug, Map.get(post, "slug")),
title: Map.get(post, :title, Map.get(post, "title")),
content:
render_post_content(
raw_content,
canonical_post_paths,
canonical_media_paths,
language,
template_context
),
raw_content: raw_content,
excerpt:
Map.get(post, :excerpt, Map.get(post, "excerpt", Map.get(post_record || %{}, :excerpt))),
author: Map.get(post, :author, Map.get(post, "author", Map.get(post_record || %{}, :author))),
language:
Map.get(
post,
:language,
Map.get(post, "language", Map.get(post_record || %{}, :language))
),
published_at:
Map.get(post, :published_at, Map.get(post, "published_at", Map.get(post_record || %{}, :published_at))),
created_at:
Map.get(post, :created_at, Map.get(post, "created_at", Map.get(post_record || %{}, :created_at))),
updated_at:
Map.get(post, :updated_at, Map.get(post, "updated_at", Map.get(post_record || %{}, :updated_at))),
tags: Map.get(post, :tags, Map.get(post, "tags", Map.get(post_record || %{}, :tags, []))) || [],
categories:
Map.get(post, :categories, Map.get(post, "categories", Map.get(post_record || %{}, :categories, []))) || [],
template_slug:
Map.get(post, :template_slug, Map.get(post, "template_slug", Map.get(post_record || %{}, :template_slug))),
do_not_translate:
Map.get(post, :do_not_translate, Map.get(post, "do_not_translate", Map.get(post_record || %{}, :do_not_translate, false))),
href: Map.get(post, :href, Map.get(post, "href")),
show_title: true,
linked_media: [],
outgoing_links: [],
incoming_links: []
}
end)
end
defp build_post_context(assigns, post_record, incoming_links, outgoing_links) do
%{
id: Map.get(assigns, :id, Map.get(assigns, "id")),
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
title: Map.get(assigns, :title, Map.get(assigns, "title")),
content: Map.get(assigns, :content, Map.get(assigns, "content")),
raw_content: Map.get(assigns, :raw_content, Map.get(assigns, "raw_content")),
excerpt:
Map.get(
assigns,
:excerpt,
Map.get(assigns, "excerpt", Map.get(post_record || %{}, :excerpt))
),
author: Map.get(post_record || %{}, :author),
language:
Map.get(
assigns,
:language,
Map.get(assigns, "language", Map.get(post_record || %{}, :language))
),
show_title: true,
published_at: Map.get(post_record || %{}, :published_at),
created_at: Map.get(post_record || %{}, :created_at),
updated_at: Map.get(post_record || %{}, :updated_at),
tags: Map.get(post_record || %{}, :tags, []) || [],
categories: Map.get(post_record || %{}, :categories, []) || [],
template_slug:
Map.get(
post_record || %{},
:template_slug,
Map.get(assigns, :template_slug, Map.get(assigns, "template_slug"))
),
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
linked_media: [],
outgoing_links: outgoing_links,
incoming_links: incoming_links
}
end
defp render_post_content(content, canonical_post_paths, canonical_media_paths, language, template_context) do
Filters.render_markdown(content, canonical_post_paths, canonical_media_paths, language, template_context)
end
defp template_render_context(project_id) do
project = Projects.get_project!(project_id)
Liquex.Context.new(%{},
static_environment: %{},
filter_module: Filters,
file_system: FileSystem.new(StarterTemplates.template_roots(project))
)
end
defp normalize_pagination(nil, posts) do
total_items = length(posts)
%{
current_page: 1,
total_pages: 1,
total_items: total_items,
items_per_page: total_items,
has_prev_page: false,
prev_page_href: "",
has_next_page: false,
next_page_href: ""
}
end
defp normalize_pagination(%{} = pagination, posts) do
total_items =
Map.get(pagination, :total_items, Map.get(pagination, "total_items", length(posts)))
items_per_page =
Map.get(pagination, :items_per_page, Map.get(pagination, "items_per_page", total_items))
%{
current_page: Map.get(pagination, :current_page, Map.get(pagination, "current_page", 1)),
total_pages: Map.get(pagination, :total_pages, Map.get(pagination, "total_pages", 1)),
total_items: total_items,
items_per_page: items_per_page,
has_prev_page:
Map.get(pagination, :has_prev_page, Map.get(pagination, "has_prev_page", false)),
prev_page_href:
Map.get(pagination, :prev_page_href, Map.get(pagination, "prev_page_href", "")),
has_next_page:
Map.get(pagination, :has_next_page, Map.get(pagination, "has_next_page", false)),
next_page_href:
Map.get(pagination, :next_page_href, Map.get(pagination, "next_page_href", ""))
}
end
defp normalize_archive_context(nil), do: nil
defp normalize_archive_context(%{} = archive_context) do
%{
kind: Map.get(archive_context, :kind, Map.get(archive_context, "kind")),
name: Map.get(archive_context, :name, Map.get(archive_context, "name")),
month: Map.get(archive_context, :month, Map.get(archive_context, "month")),
year: Map.get(archive_context, :year, Map.get(archive_context, "year")),
day: Map.get(archive_context, :day, Map.get(archive_context, "day"))
}
end
defp build_day_blocks(posts) do
grouped_blocks =
posts
|> Enum.filter(&is_integer(Map.get(&1, :created_at)))
|> Enum.group_by(&(Map.get(&1, :created_at) |> Persistence.from_unix_ms!() |> DateTime.to_date() |> Date.to_iso8601()))
|> Enum.sort_by(fn {label, _posts} -> label end)
grouped_blocks
|> Enum.with_index()
|> Enum.map(fn {{date_label, grouped_posts}, index} ->
%{
date_label: date_label,
show_date_marker: true,
show_separator: index < length(grouped_blocks) - 1,
posts: Enum.sort_by(grouped_posts, &Map.get(&1, :created_at))
}
end)
|> case do
[] -> [%{date_label: "", show_date_marker: false, show_separator: false, posts: posts}]
blocks -> blocks
end
end
defp min_date(posts) do
posts
|> Enum.map(&Map.get(&1, :created_at))
|> Enum.filter(&is_integer/1)
|> Enum.min(fn -> nil end)
end
defp max_date(posts) do
posts
|> Enum.map(&Map.get(&1, :created_at))
|> Enum.filter(&is_integer/1)
|> Enum.max(fn -> nil end)
end
defp show_archive_range_heading?(%{kind: "date"}, _day_blocks), do: true
defp show_archive_range_heading?(_archive_context, _day_blocks), do: false
defp default_pico_stylesheet_href(theme), do: PreviewAssets.stylesheet_href(theme)
defp href_for_language(""), do: "/"
defp href_for_language(prefix), do: prefix <> "/"
defp calendar_initial_year(%{created_at: created_at}) when is_integer(created_at),
do: Persistence.from_unix_ms!(created_at).year
defp calendar_initial_year(_post), do: nil
defp calendar_initial_month(%{created_at: created_at}) when is_integer(created_at),
do: Persistence.from_unix_ms!(created_at).month
defp calendar_initial_month(_post), do: nil
defp calendar_initial_year_from_posts([post | _rest]), do: calendar_initial_year(post)
defp calendar_initial_year_from_posts([]), do: nil
defp calendar_initial_month_from_posts([post | _rest]), do: calendar_initial_month(post)
defp calendar_initial_month_from_posts([]), do: nil
defp language_prefix(language, main_language) when language == main_language, do: ""
defp language_prefix(nil, _main_language), do: ""
defp language_prefix(language, _main_language), do: "/#{language}"
defp normalize_language(nil, fallback), do: fallback
defp normalize_language("", fallback), do: fallback
defp normalize_language(language, _fallback) do
language
|> to_string()
|> String.downcase()
|> String.split("-", parts: 2)
|> hd()
end
end

View File

@@ -0,0 +1,131 @@
defmodule BDS.Rendering.LinksAndLanguages do
@moduledoc false
import Ecto.Query
alias BDS.Media.Media, as: MediaAsset
alias BDS.Persistence
alias BDS.PostLinks
alias BDS.Posts.Post
alias BDS.Repo
def canonical_post_path_by_slug(project_id, main_language) do
posts =
Repo.all(
from post in Post, where: post.project_id == ^project_id and post.status == :published
)
translations =
Repo.all(
from translation in BDS.Posts.Translation,
where: translation.project_id == ^project_id and translation.status == :published
)
post_by_id = Map.new(posts, fn post -> {post.id, post} end)
post_paths =
Enum.into(posts, %{}, fn post ->
{post.slug, post_path(post, nil)}
end)
Enum.reduce(translations, post_paths, fn translation, acc ->
case Map.get(post_by_id, translation.translation_for) do
nil -> acc
post -> Map.put(acc, post.slug, post_path(post, translation.language, main_language))
end
end)
end
def canonical_media_path_by_source_path(project_id) do
Repo.all(from media in MediaAsset, where: media.project_id == ^project_id)
|> Enum.reduce(%{}, fn media, acc ->
datetime = Persistence.from_unix_ms!(media.created_at)
source_key =
Path.join([
"media",
Integer.to_string(datetime.year),
String.pad_leading(Integer.to_string(datetime.month), 2, "0"),
media.original_name
])
|> String.downcase()
Map.put(acc, source_key, "/" <> media.file_path)
end)
end
def post_path(post, language_prefix)
when is_binary(language_prefix) and language_prefix != "" do
language_prefix <> post_path(post, nil)
end
def post_path(post, ""), do: post_path(post, nil)
def post_path(post, nil) do
datetime = Persistence.from_unix_ms!(post.created_at)
Path.join([
Integer.to_string(datetime.year),
String.pad_leading(Integer.to_string(datetime.month), 2, "0"),
String.pad_leading(Integer.to_string(datetime.day), 2, "0"),
post.slug,
"index.html"
])
|> then(&("/" <> String.trim_trailing(&1, "index.html")))
end
def post_path(post, language, main_language) do
prefix = language_prefix(language, main_language)
post_path(post, prefix)
end
def link_contexts(_project_id, nil, _direction, _main_language), do: []
def link_contexts(project_id, post_id, :incoming, main_language) do
PostLinks.list_incoming_links(post_id)
|> Enum.map(&link_context(project_id, &1, :incoming, main_language))
|> Enum.reject(&is_nil/1)
end
def link_contexts(project_id, post_id, :outgoing, main_language) do
PostLinks.list_outgoing_links(post_id)
|> Enum.map(&link_context(project_id, &1, :outgoing, main_language))
|> Enum.reject(&is_nil/1)
end
defp link_context(_project_id, link, direction, main_language) do
linked_post_id =
case direction do
:incoming -> link.source_post_id
:outgoing -> link.target_post_id
end
case Repo.get(Post, linked_post_id) do
nil ->
nil
linked_post ->
%{
href: post_path(linked_post, nil),
title: linked_post.title,
display_slug: linked_post.slug,
language: normalize_language(linked_post.language, main_language)
}
end
end
def language_prefix(language, main_language) when language == main_language, do: ""
def language_prefix(nil, _main_language), do: ""
def language_prefix(language, _main_language), do: "/#{language}"
def normalize_language(nil, fallback), do: fallback
def normalize_language("", fallback), do: fallback
def normalize_language(language, _fallback) do
language
|> to_string()
|> String.downcase()
|> String.split("-", parts: 2)
|> hd()
end
end

View File

@@ -0,0 +1,295 @@
defmodule BDS.Rendering.ListArchive do
@moduledoc false
alias BDS.I18n
alias BDS.Persistence
alias BDS.Rendering.LinksAndLanguages
alias BDS.Rendering.Metadata, as: RenderMetadata
alias BDS.Rendering.PostRendering
alias BDS.Rendering.TemplateSelection
def list_assigns(project_id, assigns) do
metadata = RenderMetadata.project_metadata(project_id)
template_context = TemplateSelection.template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))
canonical_post_paths = LinksAndLanguages.canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = LinksAndLanguages.canonical_media_path_by_source_path(project_id)
posts =
normalize_list_posts(
Map.get(assigns, :posts, Map.get(assigns, "posts", [])),
canonical_post_paths,
canonical_media_paths,
language,
template_context
)
pagination =
normalize_pagination(Map.get(assigns, :pagination, Map.get(assigns, "pagination")), posts)
day_blocks = build_day_blocks(posts)
min_date = min_date(posts)
max_date = max_date(posts)
normalized_archive_context = normalize_archive_context(archive_context)
%{
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", LinksAndLanguages.language_prefix(language, main_language))
),
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title")),
posts: posts,
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme))
),
html_theme_attribute:
Map.get(
assigns,
:html_theme_attribute,
Map.get(assigns, "html_theme_attribute")
),
blog_languages: RenderMetadata.blog_languages(metadata, language),
alternate_links: [],
menu_items: RenderMetadata.menu_items(project_id),
calendar_initial_year: calendar_initial_year_from_posts(posts),
calendar_initial_month: calendar_initial_month_from_posts(posts),
archive_context: normalized_archive_context,
show_archive_range_heading: show_archive_range_heading?(normalized_archive_context, day_blocks),
min_date: min_date,
max_date: max_date,
is_list_page: true,
is_first_page: pagination.current_page <= 1,
is_last_page: pagination.current_page >= pagination.total_pages,
has_prev_page: pagination.has_prev_page,
has_next_page: pagination.has_next_page,
prev_page_href: pagination.prev_page_href,
next_page_href: pagination.next_page_href,
current_page: pagination.current_page,
total_pages: pagination.total_pages,
total_items: pagination.total_items,
items_per_page: pagination.items_per_page,
canonical_post_path_by_slug: canonical_post_paths,
canonical_media_path_by_source_path: canonical_media_paths,
post_data_json_by_id:
Enum.into(posts, %{}, fn post -> {post.id, PostRendering.post_data_json_value(post)} end),
day_blocks: day_blocks
}
end
def not_found_assigns(project_id, assigns) do
metadata = RenderMetadata.project_metadata(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
%{
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", "404")),
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", LinksAndLanguages.language_prefix(language, main_language))
),
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme))
),
html_theme_attribute:
Map.get(
assigns,
:html_theme_attribute,
Map.get(assigns, "html_theme_attribute")
),
blog_languages: RenderMetadata.blog_languages(metadata, language),
menu_items: RenderMetadata.menu_items(project_id),
alternate_links: [],
not_found_message:
Map.get(
assigns,
:not_found_message,
Map.get(
assigns,
"not_found_message",
I18n.translate(language, "render.notFound.message")
)
),
not_found_back_label:
Map.get(
assigns,
:not_found_back_label,
Map.get(
assigns,
"not_found_back_label",
I18n.translate(language, "render.notFound.back")
)
)
}
end
defp normalize_list_posts(posts, canonical_post_paths, canonical_media_paths, language, template_context) do
Enum.map(posts, fn post ->
post_record = PostRendering.load_post_record(post)
raw_content =
Map.get(
post,
:content,
Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
)
%{
id: Map.get(post, :id, Map.get(post, "id")),
slug: Map.get(post, :slug, Map.get(post, "slug")),
title: Map.get(post, :title, Map.get(post, "title")),
content:
PostRendering.render_post_content(
raw_content,
canonical_post_paths,
canonical_media_paths,
language,
template_context
),
raw_content: raw_content,
excerpt:
Map.get(post, :excerpt, Map.get(post, "excerpt", Map.get(post_record || %{}, :excerpt))),
author: Map.get(post, :author, Map.get(post, "author", Map.get(post_record || %{}, :author))),
language:
Map.get(
post,
:language,
Map.get(post, "language", Map.get(post_record || %{}, :language))
),
published_at:
Map.get(post, :published_at, Map.get(post, "published_at", Map.get(post_record || %{}, :published_at))),
created_at:
Map.get(post, :created_at, Map.get(post, "created_at", Map.get(post_record || %{}, :created_at))),
updated_at:
Map.get(post, :updated_at, Map.get(post, "updated_at", Map.get(post_record || %{}, :updated_at))),
tags: Map.get(post, :tags, Map.get(post, "tags", Map.get(post_record || %{}, :tags, []))) || [],
categories:
Map.get(post, :categories, Map.get(post, "categories", Map.get(post_record || %{}, :categories, []))) || [],
template_slug:
Map.get(post, :template_slug, Map.get(post, "template_slug", Map.get(post_record || %{}, :template_slug))),
do_not_translate:
Map.get(post, :do_not_translate, Map.get(post, "do_not_translate", Map.get(post_record || %{}, :do_not_translate, false))),
href: Map.get(post, :href, Map.get(post, "href")),
show_title: true,
linked_media: [],
outgoing_links: [],
incoming_links: []
}
end)
end
defp normalize_pagination(nil, posts) do
total_items = length(posts)
%{
current_page: 1,
total_pages: 1,
total_items: total_items,
items_per_page: total_items,
has_prev_page: false,
prev_page_href: "",
has_next_page: false,
next_page_href: ""
}
end
defp normalize_pagination(%{} = pagination, posts) do
total_items =
Map.get(pagination, :total_items, Map.get(pagination, "total_items", length(posts)))
items_per_page =
Map.get(pagination, :items_per_page, Map.get(pagination, "items_per_page", total_items))
%{
current_page: Map.get(pagination, :current_page, Map.get(pagination, "current_page", 1)),
total_pages: Map.get(pagination, :total_pages, Map.get(pagination, "total_pages", 1)),
total_items: total_items,
items_per_page: items_per_page,
has_prev_page:
Map.get(pagination, :has_prev_page, Map.get(pagination, "has_prev_page", false)),
prev_page_href:
Map.get(pagination, :prev_page_href, Map.get(pagination, "prev_page_href", "")),
has_next_page:
Map.get(pagination, :has_next_page, Map.get(pagination, "has_next_page", false)),
next_page_href:
Map.get(pagination, :next_page_href, Map.get(pagination, "next_page_href", ""))
}
end
defp normalize_archive_context(nil), do: nil
defp normalize_archive_context(%{} = archive_context) do
%{
kind: Map.get(archive_context, :kind, Map.get(archive_context, "kind")),
name: Map.get(archive_context, :name, Map.get(archive_context, "name")),
month: Map.get(archive_context, :month, Map.get(archive_context, "month")),
year: Map.get(archive_context, :year, Map.get(archive_context, "year")),
day: Map.get(archive_context, :day, Map.get(archive_context, "day"))
}
end
defp build_day_blocks(posts) do
grouped_blocks =
posts
|> Enum.filter(&is_integer(Map.get(&1, :created_at)))
|> Enum.group_by(&(Map.get(&1, :created_at) |> Persistence.from_unix_ms!() |> DateTime.to_date() |> Date.to_iso8601()))
|> Enum.sort_by(fn {label, _posts} -> label end)
grouped_blocks
|> Enum.with_index()
|> Enum.map(fn {{date_label, grouped_posts}, index} ->
%{
date_label: date_label,
show_date_marker: true,
show_separator: index < length(grouped_blocks) - 1,
posts: Enum.sort_by(grouped_posts, &Map.get(&1, :created_at))
}
end)
|> case do
[] -> [%{date_label: "", show_date_marker: false, show_separator: false, posts: posts}]
blocks -> blocks
end
end
defp min_date(posts) do
posts
|> Enum.map(&Map.get(&1, :created_at))
|> Enum.filter(&is_integer/1)
|> Enum.min(fn -> nil end)
end
defp max_date(posts) do
posts
|> Enum.map(&Map.get(&1, :created_at))
|> Enum.filter(&is_integer/1)
|> Enum.max(fn -> nil end)
end
defp show_archive_range_heading?(%{kind: "date"}, _day_blocks), do: true
defp show_archive_range_heading?(_archive_context, _day_blocks), do: false
defp calendar_initial_year_from_posts([post | _rest]), do: RenderMetadata.calendar_initial_year(post)
defp calendar_initial_year_from_posts([]), do: nil
defp calendar_initial_month_from_posts([post | _rest]), do: RenderMetadata.calendar_initial_month(post)
defp calendar_initial_month_from_posts([]), do: nil
end

View File

@@ -0,0 +1,113 @@
defmodule BDS.Rendering.Metadata do
@moduledoc false
import Ecto.Query
alias BDS.I18n
alias BDS.Menu
alias BDS.Metadata, as: ProjectMetadata
alias BDS.Persistence
alias BDS.PreviewAssets
alias BDS.Rendering.LinksAndLanguages
alias BDS.Repo
alias BDS.Posts.Post
alias BDS.Posts.Translation
alias BDS.Tags.Tag
def project_metadata(project_id) do
{:ok, metadata} = ProjectMetadata.get_project_metadata(project_id)
metadata
end
def menu_items(project_id) do
{:ok, %{items: items}} = Menu.get_menu(project_id)
Enum.map(items, &to_template_menu_item/1)
end
defp to_template_menu_item(item) do
kind = Map.get(item, :kind)
children = Enum.map(Map.get(item, :children, []), &to_template_menu_item/1)
%{
title: Map.get(item, :label, ""),
href: menu_item_href(item),
has_children: children != [],
children: children,
kind: kind
}
end
defp menu_item_href(%{kind: :home}), do: "/"
defp menu_item_href(%{kind: :page, slug: slug}) when is_binary(slug) and slug != "",
do: "/#{slug}/"
defp menu_item_href(%{kind: :category_archive, slug: slug}) when is_binary(slug) and slug != "",
do: "/category/#{URI.encode(slug)}/"
defp menu_item_href(%{kind: :submenu}), do: "#"
defp menu_item_href(_item), do: "#"
def blog_languages(metadata, current_language) do
([metadata.main_language] ++ (metadata.blog_languages || []))
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.uniq()
|> Enum.map(fn language ->
normalized = I18n.normalize_language(language)
href_prefix = LinksAndLanguages.language_prefix(normalized, metadata.main_language || current_language)
%{
code: normalized,
flag: I18n.flag(normalized),
href: href_for_language(href_prefix),
href_prefix: href_prefix,
is_current: normalized == I18n.normalize_language(current_language)
}
end)
end
def tag_color_by_name(project_id) do
Repo.all(from tag in Tag, where: tag.project_id == ^project_id, select: {tag.name, tag.color})
|> Enum.into(%{}, fn {name, color} -> {name, color} end)
end
def alternate_links(nil, _project_id, _main_language), do: []
def alternate_links(%Post{} = post, project_id, main_language) do
translations =
Repo.all(
from translation in Translation,
where:
translation.project_id == ^project_id and
translation.translation_for == ^post.id and
translation.status == :published,
order_by: [asc: translation.language]
)
[%{href: LinksAndLanguages.post_path(post, nil), hreflang: LinksAndLanguages.normalize_language(post.language, main_language)}] ++
Enum.map(translations, fn translation ->
%{href: LinksAndLanguages.post_path(post, translation.language, main_language), hreflang: translation.language}
end)
end
def backlinks(incoming_links) do
Enum.map(incoming_links, fn link ->
%{path: link.href, display_slug: link.display_slug, title: link.title}
end)
end
def default_pico_stylesheet_href(theme), do: PreviewAssets.stylesheet_href(theme)
def href_for_language(""), do: "/"
def href_for_language(prefix), do: prefix <> "/"
def calendar_initial_year(%{created_at: created_at}) when is_integer(created_at),
do: Persistence.from_unix_ms!(created_at).year
def calendar_initial_year(_post), do: nil
def calendar_initial_month(%{created_at: created_at}) when is_integer(created_at),
do: Persistence.from_unix_ms!(created_at).month
def calendar_initial_month(_post), do: nil
end

View File

@@ -0,0 +1,167 @@
defmodule BDS.Rendering.PostRendering do
@moduledoc false
alias BDS.Rendering.Filters
alias BDS.Rendering.LinksAndLanguages
alias BDS.Rendering.Metadata, as: RenderMetadata
alias BDS.Rendering.TemplateSelection
alias BDS.Posts.Post
alias BDS.Posts.Translation
alias BDS.Repo
def post_assigns(project_id, assigns) do
metadata = RenderMetadata.project_metadata(project_id)
template_context = TemplateSelection.template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
post_record = load_post_record(assigns)
canonical_post = canonical_post_record(post_record)
post_id = canonical_post_id(post_record, assigns)
post_categories = Map.get(post_record || %{}, :categories, []) || []
post_tags = Map.get(post_record || %{}, :tags, []) || []
canonical_post_paths = LinksAndLanguages.canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = LinksAndLanguages.canonical_media_path_by_source_path(project_id)
raw_content = Map.get(assigns, :content, Map.get(assigns, "content"))
rendered_content = render_post_content(raw_content, canonical_post_paths, canonical_media_paths, language, template_context)
incoming_links = LinksAndLanguages.link_contexts(project_id, post_id, :incoming, main_language)
outgoing_links = LinksAndLanguages.link_contexts(project_id, post_id, :outgoing, main_language)
post_assigns =
assigns
|> Map.put(:content, rendered_content)
|> Map.put(:raw_content, raw_content)
%{
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", LinksAndLanguages.language_prefix(language, main_language))
),
page_title:
Map.get(
assigns,
:page_title,
Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))
),
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme))
),
html_theme_attribute:
Map.get(
assigns,
:html_theme_attribute,
Map.get(assigns, "html_theme_attribute")
),
blog_languages: RenderMetadata.blog_languages(metadata, language),
alternate_links: RenderMetadata.alternate_links(canonical_post, project_id, main_language),
menu_items: RenderMetadata.menu_items(project_id),
calendar_initial_year: RenderMetadata.calendar_initial_year(post_record),
calendar_initial_month: RenderMetadata.calendar_initial_month(post_record),
post_categories: post_categories,
post_tags: post_tags,
tag_color_by_name: RenderMetadata.tag_color_by_name(project_id),
backlinks: RenderMetadata.backlinks(incoming_links),
canonical_post_path_by_slug: canonical_post_paths,
canonical_media_path_by_source_path: canonical_media_paths,
post_data_json_by_id: post_data_json(post_assigns, post_record),
post: build_post_context(post_assigns, post_record, incoming_links, outgoing_links)
}
end
def load_post_record(assigns) do
case Map.get(assigns, :id, Map.get(assigns, "id")) do
nil -> nil
post_id -> Repo.get(Post, post_id) || Repo.get(Translation, post_id)
end
end
defp canonical_post_record(%Translation{translation_for: post_id}), do: Repo.get(Post, post_id)
defp canonical_post_record(%Post{} = post), do: post
defp canonical_post_record(_other), do: nil
defp canonical_post_id(%Translation{translation_for: post_id}, _assigns), do: post_id
defp canonical_post_id(%Post{id: post_id}, _assigns), do: post_id
defp canonical_post_id(_post_record, assigns), do: Map.get(assigns, :id, Map.get(assigns, "id"))
defp post_data_json(assigns, post_record) do
id = Map.get(assigns, :id, Map.get(assigns, "id"))
if is_binary(id) do
incoming_links = LinksAndLanguages.link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :incoming, Map.get(post_record || %{}, :language))
outgoing_links = LinksAndLanguages.link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :outgoing, Map.get(post_record || %{}, :language))
%{
id => post_data_json_value(build_post_context(assigns, post_record, incoming_links, outgoing_links))
}
else
%{}
end
end
def post_data_json_value(post_context) do
Jason.encode!(%{
id: Map.get(post_context, :id),
title: Map.get(post_context, :title),
slug: Map.get(post_context, :slug),
excerpt: Map.get(post_context, :excerpt),
author: Map.get(post_context, :author),
language: Map.get(post_context, :language),
published_at: Map.get(post_context, :published_at),
created_at: Map.get(post_context, :created_at),
updated_at: Map.get(post_context, :updated_at),
tags: Map.get(post_context, :tags, []),
categories: Map.get(post_context, :categories, [])
})
end
defp build_post_context(assigns, post_record, incoming_links, outgoing_links) do
%{
id: Map.get(assigns, :id, Map.get(assigns, "id")),
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
title: Map.get(assigns, :title, Map.get(assigns, "title")),
content: Map.get(assigns, :content, Map.get(assigns, "content")),
raw_content: Map.get(assigns, :raw_content, Map.get(assigns, "raw_content")),
excerpt:
Map.get(
assigns,
:excerpt,
Map.get(assigns, "excerpt", Map.get(post_record || %{}, :excerpt))
),
author: Map.get(post_record || %{}, :author),
language:
Map.get(
assigns,
:language,
Map.get(assigns, "language", Map.get(post_record || %{}, :language))
),
show_title: true,
published_at: Map.get(post_record || %{}, :published_at),
created_at: Map.get(post_record || %{}, :created_at),
updated_at: Map.get(post_record || %{}, :updated_at),
tags: Map.get(post_record || %{}, :tags, []) || [],
categories: Map.get(post_record || %{}, :categories, []) || [],
template_slug:
Map.get(
post_record || %{},
:template_slug,
Map.get(assigns, :template_slug, Map.get(assigns, "template_slug"))
),
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
linked_media: [],
outgoing_links: outgoing_links,
incoming_links: incoming_links
}
end
def render_post_content(content, canonical_post_paths, canonical_media_paths, language, template_context) do
Filters.render_markdown(content, canonical_post_paths, canonical_media_paths, language, template_context)
end
end

View File

@@ -0,0 +1,153 @@
defmodule BDS.Rendering.TemplateSelection do
@moduledoc false
import Ecto.Query
alias BDS.Frontmatter
alias BDS.Projects
alias BDS.Rendering.FileSystem
alias BDS.Rendering.Filters
alias BDS.Repo
alias BDS.StarterTemplates
alias BDS.Templates.Template
def load_template_source(project_id, kind, slug) do
project = Projects.get_project!(project_id)
case select_template(project_id, kind, slug) do
%Template{} = template ->
case published_template_body(template) do
{:ok, _source} = ok ->
ok
{:error, reason} = error ->
maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
end
nil ->
load_bundled_template_source(project, kind, slug)
end
end
defp select_template(project_id, kind, slug) when is_binary(slug) and slug != "" do
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == ^kind and
template.status == :published and
template.enabled == true and template.slug == ^slug,
limit: 1
)
end
defp select_template(project_id, :post, nil) do
case StarterTemplates.default_slug(:post) do
nil ->
nil
default_slug ->
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == :post and
template.status == :published and
template.enabled == true and template.slug == ^default_slug,
limit: 1
)
end
end
defp select_template(project_id, kind, nil) do
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == ^kind and
template.status == :published and
template.enabled == true,
order_by: [desc: template.created_at, desc: template.slug],
limit: 1
)
end
defp published_template_body(%Template{content: content}) when is_binary(content),
do: {:ok, content}
defp published_template_body(%Template{} = template) do
project = Projects.get_project!(template.project_id)
full_path = Path.join(Projects.project_data_dir(project), template.file_path)
case File.read(full_path) do
{:ok, contents} ->
case Frontmatter.parse_document(contents) do
{:ok, %{body: body}} -> {:ok, body}
{:error, reason} -> {:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
def render_template(project_id, source, assigns) do
with {:ok, template_ast} <- Liquex.parse(source) do
project = Projects.get_project!(project_id)
context =
Liquex.Context.new(assigns,
static_environment: assigns,
filter_module: Filters,
file_system: FileSystem.new(StarterTemplates.template_roots(project))
)
{result, _context} = Liquex.render!(template_ast, context)
{:ok, IO.iodata_to_binary(result)}
end
rescue
error -> {:error, error}
end
defp load_bundled_template_source(project, kind, slug) do
desired_slug = bundled_template_slug(kind, slug)
if is_binary(desired_slug) do
file_system = project |> StarterTemplates.template_roots() |> FileSystem.new()
source = Liquex.FileSystem.read_template_file(file_system, desired_slug)
case Frontmatter.parse_document(source) do
{:ok, %{body: body}} -> {:ok, body}
{:error, :invalid_frontmatter} -> {:ok, source}
end
else
{:error, :template_not_found}
end
rescue
error in [Liquex.Error] ->
_ = error
{:error, :template_not_found}
end
defp maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
when reason in [:enoent, :template_not_found] do
if template.content in [nil, ""] and StarterTemplates.default_template?(kind, template.slug) do
load_bundled_template_source(project, kind, slug)
else
error
end
end
defp maybe_load_bundled_template_source(_project, _kind, _slug, _template, _reason, error),
do: error
defp bundled_template_slug(_kind, slug) when is_binary(slug) and slug != "", do: slug
defp bundled_template_slug(kind, _slug), do: StarterTemplates.default_slug(kind)
def template_render_context(project_id) do
project = Projects.get_project!(project_id)
Liquex.Context.new(%{},
static_environment: %{},
filter_module: Filters,
file_system: FileSystem.new(StarterTemplates.template_roots(project))
)
end
end