feat: PLAN step 1 done, supposedly
This commit is contained in:
@@ -56,7 +56,11 @@ defmodule BDS.Frontmatter do
|
||||
Integer.to_string(value)
|
||||
end
|
||||
|
||||
["#{key}: #{rendered}"]
|
||||
if timestamp_key?(key) do
|
||||
["#{key}: '#{rendered}'"]
|
||||
else
|
||||
["#{key}: #{rendered}"]
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_field({key, value}) do
|
||||
@@ -196,7 +200,7 @@ defmodule BDS.Frontmatter do
|
||||
|
||||
defp serialize_scalar(key, value) when is_integer(value) do
|
||||
if is_binary(key) and timestamp_key?(key) do
|
||||
Persistence.timestamp_to_iso8601(value)
|
||||
"'#{Persistence.timestamp_to_iso8601(value)}'"
|
||||
else
|
||||
Integer.to_string(value)
|
||||
end
|
||||
|
||||
@@ -238,7 +238,13 @@ defmodule BDS.Generation do
|
||||
|
||||
single_outputs =
|
||||
if :single in plan.sections do
|
||||
build_single_outputs(plan.project_id, published_posts, published_translations, post_by_id)
|
||||
build_single_outputs(
|
||||
plan.project_id,
|
||||
plan.language,
|
||||
published_posts,
|
||||
published_translations,
|
||||
post_by_id
|
||||
)
|
||||
else
|
||||
[]
|
||||
end
|
||||
@@ -439,7 +445,14 @@ defmodule BDS.Generation do
|
||||
Enum.map(languages, fn language ->
|
||||
{
|
||||
archive_path(route_language(plan.language, language), [year], 1),
|
||||
render_date_archive_page(plan, year, posts, language, pagination)
|
||||
render_date_archive_page(
|
||||
plan,
|
||||
year,
|
||||
%{kind: "year", year: String.to_integer(year)},
|
||||
posts,
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
}
|
||||
end)
|
||||
end)
|
||||
@@ -451,7 +464,14 @@ defmodule BDS.Generation do
|
||||
Enum.map(languages, fn language ->
|
||||
{
|
||||
archive_path(route_language(plan.language, language), [year, month], 1),
|
||||
render_date_archive_page(plan, "#{year}-#{month}", posts, language, pagination)
|
||||
render_date_archive_page(
|
||||
plan,
|
||||
"#{year}-#{month}",
|
||||
%{kind: "month", year: String.to_integer(year), month: String.to_integer(month)},
|
||||
posts,
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
}
|
||||
end)
|
||||
end)
|
||||
@@ -505,62 +525,100 @@ defmodule BDS.Generation do
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_single_outputs(project_id, published_posts, published_translations, post_by_id) do
|
||||
defp build_single_outputs(
|
||||
project_id,
|
||||
main_language,
|
||||
published_posts,
|
||||
published_translations,
|
||||
post_by_id
|
||||
) do
|
||||
translations_by_post_language =
|
||||
Map.new(published_translations, fn translation ->
|
||||
{{translation.translation_for, translation.language}, translation}
|
||||
end)
|
||||
|
||||
post_outputs =
|
||||
Enum.map(published_posts, fn post ->
|
||||
body = load_body(project_id, post.file_path, post.content)
|
||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||
|
||||
{post_output_path(post),
|
||||
render_post_output(
|
||||
project_id,
|
||||
post.template_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
id: canonical_variant.id,
|
||||
title: canonical_variant.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: post.language,
|
||||
excerpt: post.excerpt
|
||||
language: canonical_variant.language,
|
||||
excerpt: canonical_variant.excerpt
|
||||
},
|
||||
fn ->
|
||||
render_post_page(post.title, body, post.slug, post.language)
|
||||
render_post_page(canonical_variant.title, body, post.slug, canonical_variant.language)
|
||||
end
|
||||
)}
|
||||
end)
|
||||
|
||||
translation_outputs =
|
||||
Enum.flat_map(published_translations, fn translation ->
|
||||
case post_by_id[translation.translation_for] do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
post ->
|
||||
body = load_body(project_id, translation.file_path, translation.content)
|
||||
|
||||
[
|
||||
{post_output_path(post, translation.language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
post.template_slug,
|
||||
%{
|
||||
id: translation.id,
|
||||
title: translation.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: translation.language,
|
||||
excerpt: translation.excerpt
|
||||
},
|
||||
fn ->
|
||||
render_post_page(translation.title, body, post.slug, translation.language)
|
||||
end
|
||||
)}
|
||||
]
|
||||
end
|
||||
end)
|
||||
post_outputs_for_noncanonical_variants(
|
||||
project_id,
|
||||
main_language,
|
||||
published_posts,
|
||||
published_translations,
|
||||
post_by_id
|
||||
)
|
||||
|
||||
post_outputs ++ translation_outputs
|
||||
end
|
||||
|
||||
defp post_outputs_for_noncanonical_variants(
|
||||
project_id,
|
||||
main_language,
|
||||
published_posts,
|
||||
published_translations,
|
||||
post_by_id
|
||||
) do
|
||||
Enum.flat_map(published_posts, fn post ->
|
||||
post_variant =
|
||||
if post.language == main_language do
|
||||
[]
|
||||
else
|
||||
[{post.language, post}]
|
||||
end
|
||||
|
||||
translation_variants =
|
||||
published_translations
|
||||
|> Enum.filter(&(&1.translation_for == post.id and &1.language != main_language))
|
||||
|> Enum.map(&{&1.language, &1})
|
||||
|
||||
(post_variant ++ translation_variants)
|
||||
|> Enum.flat_map(fn {language, variant} ->
|
||||
canonical_post = Map.get(post_by_id, post.id, post)
|
||||
body = load_body(project_id, variant.file_path, variant.content)
|
||||
|
||||
[
|
||||
{post_output_path(canonical_post, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
canonical_post.template_slug,
|
||||
%{
|
||||
id: variant.id,
|
||||
title: variant.title,
|
||||
content: body,
|
||||
slug: canonical_post.slug,
|
||||
language: variant.language,
|
||||
excerpt: variant.excerpt
|
||||
},
|
||||
fn ->
|
||||
render_post_page(variant.title, body, canonical_post.slug, variant.language)
|
||||
end
|
||||
)}
|
||||
]
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp list_published_posts(project_id) do
|
||||
Repo.all(
|
||||
from post in Post,
|
||||
@@ -778,7 +836,7 @@ defmodule BDS.Generation do
|
||||
)
|
||||
end
|
||||
|
||||
defp render_date_archive_page(plan, label, posts, language, pagination) do
|
||||
defp render_date_archive_page(plan, label, archive_context, posts, language, pagination) do
|
||||
fallback = fn ->
|
||||
items =
|
||||
posts
|
||||
@@ -801,18 +859,8 @@ defmodule BDS.Generation do
|
||||
plan,
|
||||
language,
|
||||
label,
|
||||
Enum.map(posts, fn post ->
|
||||
%{
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
href: "#",
|
||||
excerpt: post.excerpt,
|
||||
content: nil,
|
||||
language: post.language
|
||||
}
|
||||
end),
|
||||
%{kind: "date", name: label},
|
||||
build_list_posts(plan.base_url, posts, route_language(plan.language, language)),
|
||||
archive_context,
|
||||
pagination,
|
||||
fallback
|
||||
)
|
||||
|
||||
@@ -367,16 +367,25 @@ defmodule BDS.Maintenance do
|
||||
end
|
||||
|
||||
defp diff_field(name, db_value, file_value) do
|
||||
db_value = stringify_value(db_value)
|
||||
file_value = stringify_value(file_value)
|
||||
|
||||
if db_value == file_value do
|
||||
if equal_diff_values?(db_value, file_value) do
|
||||
nil
|
||||
else
|
||||
%{name: name, db_value: db_value, file_value: file_value}
|
||||
%{name: name, db_value: stringify_value(db_value), file_value: stringify_value(file_value)}
|
||||
end
|
||||
end
|
||||
|
||||
defp equal_diff_values?(left, right) when is_list(left) and is_list(right) do
|
||||
normalize_list_diff_values(left) == normalize_list_diff_values(right)
|
||||
end
|
||||
|
||||
defp equal_diff_values?(left, right), do: stringify_value(left) == stringify_value(right)
|
||||
|
||||
defp normalize_list_diff_values(values) do
|
||||
values
|
||||
|> Enum.map(&stringify_value/1)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp stringify_value(nil), do: ""
|
||||
defp stringify_value(value) when is_atom(value), do: Atom.to_string(value)
|
||||
defp stringify_value(value) when is_boolean(value), do: to_string(value)
|
||||
|
||||
@@ -4,7 +4,9 @@ defmodule BDS.Preview do
|
||||
use GenServer
|
||||
|
||||
alias BDS.Posts
|
||||
alias BDS.Posts.Translation
|
||||
alias BDS.Projects
|
||||
alias BDS.Repo
|
||||
alias BDS.Rendering
|
||||
|
||||
@host "127.0.0.1"
|
||||
@@ -34,24 +36,9 @@ defmodule BDS.Preview do
|
||||
|
||||
def preview_draft(project_id, request_path, post_id)
|
||||
when is_binary(project_id) and is_binary(request_path) and is_binary(post_id) do
|
||||
post = Posts.get_post!(post_id)
|
||||
{_path, query_params} = split_request_path(request_path)
|
||||
|
||||
GenServer.call(__MODULE__, {
|
||||
:preview_draft,
|
||||
project_id,
|
||||
query_params,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: post.content || "",
|
||||
body: post.content || "",
|
||||
slug: post.slug,
|
||||
language: post.language,
|
||||
excerpt: post.excerpt,
|
||||
template_slug: post.template_slug
|
||||
}
|
||||
})
|
||||
GenServer.call(__MODULE__, {:preview_draft, project_id, query_params, post_id})
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -108,12 +95,13 @@ defmodule BDS.Preview do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:preview_draft, project_id, query_params, post}, _from, state) do
|
||||
with :ok <- ensure_running(state.current, project_id) do
|
||||
def handle_call({:preview_draft, project_id, query_params, post_id}, _from, state) do
|
||||
with :ok <- ensure_running(state.current, project_id),
|
||||
{:ok, payload} <- load_draft_preview_payload(project_id, post_id, query_params) do
|
||||
body =
|
||||
case Rendering.render_post_page(project_id, Map.get(post, :template_slug), post) do
|
||||
case Rendering.render_post_page(project_id, Map.get(payload, :template_slug), payload) do
|
||||
{:ok, rendered} -> rendered
|
||||
{:error, _reason} -> render_draft(post)
|
||||
{:error, _reason} -> render_draft(payload)
|
||||
end
|
||||
|> apply_preview_overrides(query_params)
|
||||
|
||||
@@ -124,6 +112,7 @@ defmodule BDS.Preview do
|
||||
|
||||
{:reply, {:ok, response}, state}
|
||||
else
|
||||
{:error, :not_found} = error -> {:reply, error, state}
|
||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
@@ -174,11 +163,50 @@ defmodule BDS.Preview do
|
||||
end
|
||||
|
||||
defp resolve_draft_request(project_id, post_id, query_params) do
|
||||
with {:ok, payload} <- load_draft_preview_payload(project_id, post_id, query_params) do
|
||||
body =
|
||||
case Rendering.render_post_page(project_id, Map.get(payload, :template_slug), payload) do
|
||||
{:ok, rendered} -> rendered
|
||||
{:error, _reason} -> render_draft(payload)
|
||||
end
|
||||
|> apply_preview_overrides(query_params)
|
||||
|
||||
{:ok, %{content_type: "text/html", body: body}}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_draft_preview_payload(project_id, post_id, query_params) do
|
||||
try do
|
||||
post = Posts.get_post!(post_id)
|
||||
|
||||
if post.project_id == project_id do
|
||||
payload = %{
|
||||
{:ok, draft_preview_payload(post, query_params)}
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp draft_preview_payload(post, query_params) do
|
||||
requested_language = query_params |> Map.get("lang") |> normalize_requested_language()
|
||||
|
||||
case draft_preview_translation(post.id, requested_language, post.language) do
|
||||
%Translation{} = translation ->
|
||||
%{
|
||||
id: translation.id,
|
||||
title: translation.title,
|
||||
content: translation.content || "",
|
||||
body: translation.content || "",
|
||||
slug: post.slug,
|
||||
language: translation.language,
|
||||
excerpt: translation.excerpt,
|
||||
template_slug: post.template_slug
|
||||
}
|
||||
|
||||
nil ->
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: post.content || "",
|
||||
@@ -188,23 +216,20 @@ defmodule BDS.Preview do
|
||||
excerpt: post.excerpt,
|
||||
template_slug: post.template_slug
|
||||
}
|
||||
|
||||
body =
|
||||
case Rendering.render_post_page(project_id, post.template_slug, payload) do
|
||||
{:ok, rendered} -> rendered
|
||||
{:error, _reason} -> render_draft(payload)
|
||||
end
|
||||
|> apply_preview_overrides(query_params)
|
||||
|
||||
{:ok, %{content_type: "text/html", body: body}}
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp draft_preview_translation(_post_id, nil, _post_language), do: nil
|
||||
defp draft_preview_translation(_post_id, requested_language, post_language) when requested_language == post_language, do: nil
|
||||
|
||||
defp draft_preview_translation(post_id, requested_language, _post_language) do
|
||||
Repo.get_by(Translation, translation_for: post_id, language: requested_language)
|
||||
end
|
||||
|
||||
defp normalize_requested_language(nil), do: nil
|
||||
defp normalize_requested_language(""), do: nil
|
||||
defp normalize_requested_language(language), do: language |> String.trim() |> String.downcase()
|
||||
|
||||
defp route_request(request_path) do
|
||||
normalized = request_path |> URI.parse() |> Map.get(:path, "/")
|
||||
segments = String.split(normalized, "/", trim: true)
|
||||
|
||||
@@ -500,6 +500,8 @@ defmodule BDS.Rendering 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)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule BDS.Rendering.Filters do
|
||||
use Liquex.Filter
|
||||
|
||||
alias BDS.I18n
|
||||
alias BDS.Slug
|
||||
|
||||
def i18n(value, language, _context) do
|
||||
key = value |> to_string() |> String.trim()
|
||||
@@ -32,6 +33,12 @@ defmodule BDS.Rendering.Filters do
|
||||
|> rewrite_rendered_html_urls(canonical_post_paths || %{}, canonical_media_paths || %{})
|
||||
end
|
||||
|
||||
def slugify(value, _context) do
|
||||
value
|
||||
|> to_string()
|
||||
|> Slug.slugify()
|
||||
end
|
||||
|
||||
defp replace_built_in_macros(content, language, context) do
|
||||
Regex.replace(~r/\[\[(\w+)(?:\s+([^\]]+))?\]\]/, content, fn full_match,
|
||||
macro_name,
|
||||
|
||||
@@ -4,12 +4,18 @@ defmodule BDS.Sidecar do
|
||||
alias BDS.Persistence
|
||||
|
||||
@list_item_prefix " - "
|
||||
@document_marker "---"
|
||||
@always_quoted_keys MapSet.new(["originalName", "title", "alt", "caption", "author"])
|
||||
|
||||
def serialize_document(fields) when is_list(fields) do
|
||||
fields
|
||||
|> Enum.flat_map(&serialize_field/1)
|
||||
serialized_fields =
|
||||
fields
|
||||
|> Enum.flat_map(&serialize_field/1)
|
||||
|> Enum.join("\n")
|
||||
|
||||
[@document_marker, serialized_fields, @document_marker]
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
|> Enum.join("\n")
|
||||
|> Kernel.<>("\n")
|
||||
end
|
||||
|
||||
def parse_document(contents) when is_binary(contents) do
|
||||
@@ -23,7 +29,8 @@ defmodule BDS.Sidecar do
|
||||
defp serialize_field({_key, ""}), do: []
|
||||
|
||||
defp serialize_field({key, values}) when is_list(values) do
|
||||
["#{key}:" | Enum.map(values, &" - #{serialize_scalar(nil, &1)}")]
|
||||
serialized_values = values |> Enum.map(&serialize_inline_list_scalar/1) |> Enum.join(", ")
|
||||
["#{key}: [#{serialized_values}]"]
|
||||
end
|
||||
|
||||
defp serialize_field({key, value}) when is_boolean(value) do
|
||||
@@ -104,6 +111,10 @@ defmodule BDS.Sidecar do
|
||||
defp parse_generic_scalar("false"), do: false
|
||||
defp parse_generic_scalar("[]"), do: []
|
||||
|
||||
defp parse_generic_scalar("[" <> _rest = value) do
|
||||
parse_inline_list(value)
|
||||
end
|
||||
|
||||
defp parse_generic_scalar(value) do
|
||||
if Regex.match?(~r/^-?\d+$/, value) do
|
||||
String.to_integer(value)
|
||||
@@ -142,26 +153,77 @@ defmodule BDS.Sidecar do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_scalar(_key, value) do
|
||||
defp serialize_scalar(key, value) do
|
||||
string_value = to_string(value)
|
||||
|
||||
if is_binary(key) and MapSet.member?(@always_quoted_keys, key) do
|
||||
quote_string(string_value)
|
||||
else
|
||||
maybe_quote_string(string_value)
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_inline_list_scalar(value) when is_binary(value), do: quote_string(value)
|
||||
defp serialize_inline_list_scalar(value), do: serialize_scalar(nil, value)
|
||||
|
||||
defp parse_inline_list(value) do
|
||||
inner =
|
||||
value
|
||||
|> String.trim()
|
||||
|> String.trim_leading("[")
|
||||
|> String.trim_trailing("]")
|
||||
|> String.trim()
|
||||
|
||||
if inner == "" do
|
||||
[]
|
||||
else
|
||||
parse_inline_list_items(inner)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_inline_list_items(inner) do
|
||||
if String.contains?(inner, "\"") or String.contains?(inner, "'") do
|
||||
Regex.scan(~r/"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'/, inner, capture: :all_but_first)
|
||||
|> Enum.map(fn captures ->
|
||||
captures
|
||||
|> Enum.find(&(&1 != ""))
|
||||
|> parse_inline_string_item()
|
||||
end)
|
||||
else
|
||||
inner
|
||||
|> String.split(",", trim: true)
|
||||
|> Enum.map(&(String.trim(&1) |> parse_scalar(nil)))
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_inline_string_item(nil), do: ""
|
||||
|
||||
defp parse_inline_string_item(value) do
|
||||
value
|
||||
|> to_string()
|
||||
|> maybe_quote_string()
|
||||
|> String.replace("\\n", "\n")
|
||||
|> String.replace("\\\"", "\"")
|
||||
|> String.replace("\\'", "'")
|
||||
|> String.replace("\\\\", "\\")
|
||||
end
|
||||
|
||||
defp maybe_quote_string(value) do
|
||||
if Regex.match?(~r/^[\p{L}\p{N} ._\/-]+$/u, value) do
|
||||
value
|
||||
else
|
||||
escaped =
|
||||
value
|
||||
|> String.replace("\\", "\\\\")
|
||||
|> String.replace("\"", "\\\"")
|
||||
|> String.replace("\n", "\\n")
|
||||
|
||||
"\"#{escaped}\""
|
||||
quote_string(value)
|
||||
end
|
||||
end
|
||||
|
||||
defp quote_string(value) do
|
||||
escaped =
|
||||
value
|
||||
|> String.replace("\\", "\\\\")
|
||||
|> String.replace("\"", "\\\"")
|
||||
|> String.replace("\n", "\\n")
|
||||
|
||||
"\"#{escaped}\""
|
||||
end
|
||||
|
||||
defp timestamp_key?(key) do
|
||||
rendered = to_string(key)
|
||||
String.ends_with?(rendered, "_at") or String.ends_with?(rendered, "At")
|
||||
|
||||
Reference in New Issue
Block a user