fix: incremental rendering more on par with bDS
This commit is contained in:
@@ -263,27 +263,39 @@ defmodule BDS.Desktop.ShellCommands do
|
||||
end
|
||||
|
||||
defp dispatch("apply_site_validation", project, params) do
|
||||
report = normalize_apply_validation_report(BDS.MapUtils.attr(params, :report, %{}))
|
||||
validation_report = normalize_apply_validation_report(BDS.MapUtils.attr(params, :report, %{}))
|
||||
group_id = task_group_id("apply_site_validation")
|
||||
attrs = %{group_id: group_id, group_name: "Apply Site Validation"}
|
||||
|
||||
queue_task(
|
||||
project,
|
||||
"apply_site_validation",
|
||||
"Apply Site Validation",
|
||||
"Generation",
|
||||
fn report_fn ->
|
||||
{:ok, _apply} =
|
||||
Generation.apply_validation(project.id, report,
|
||||
on_progress: scaled_progress_reporter(report_fn, 0.0, 0.85)
|
||||
)
|
||||
{:ok, prepare_task} =
|
||||
Tasks.submit_task(
|
||||
"Prepare Validation Apply",
|
||||
fn report ->
|
||||
{:ok, preparation} =
|
||||
Generation.prepare_validation_apply(project.id, validation_report,
|
||||
on_progress: report
|
||||
)
|
||||
|
||||
{:ok, validation} =
|
||||
Generation.validate_site(project.id, @site_sections,
|
||||
on_progress: scaled_progress_reporter(report_fn, 0.85, 1.0)
|
||||
)
|
||||
Map.put(preparation, :project_id, project.id)
|
||||
end,
|
||||
attrs
|
||||
)
|
||||
|
||||
site_validation_result(project.id, validation)
|
||||
end
|
||||
)
|
||||
Task.start(fn ->
|
||||
run_apply_validation_sequence(project, validation_report, group_id, attrs, prepare_task.id)
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
kind: "task_queued",
|
||||
action: "apply_site_validation",
|
||||
title: "Apply Site Validation",
|
||||
message: "Apply Site Validation tasks queued",
|
||||
project_id: project.id,
|
||||
task_id: prepare_task.id,
|
||||
task_group_id: group_id,
|
||||
panel_tab: "tasks"
|
||||
}}
|
||||
end
|
||||
|
||||
defp dispatch("metadata_diff", project, _params) do
|
||||
@@ -463,6 +475,124 @@ defmodule BDS.Desktop.ShellCommands do
|
||||
end
|
||||
end
|
||||
|
||||
defp run_apply_validation_sequence(project, validation_report, group_id, attrs, prepare_task_id) do
|
||||
with :ok <-
|
||||
wait_for_group_phase(group_id, ["Prepare Validation Apply"], @rebuild_phase_timeout),
|
||||
{:ok, preparation} <- completed_task_result(prepare_task_id),
|
||||
:ok <-
|
||||
run_apply_validation_render_tasks(
|
||||
project,
|
||||
validation_report,
|
||||
group_id,
|
||||
attrs,
|
||||
preparation
|
||||
) do
|
||||
if validation_apply_changed?(preparation) do
|
||||
:ok = run_apply_validation_refresh_task(project, group_id, attrs, :calendar)
|
||||
end
|
||||
|
||||
submit_apply_validation_check_task(project, attrs)
|
||||
else
|
||||
_other -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp run_apply_validation_render_tasks(project, validation_report, group_id, attrs, preparation) do
|
||||
sections = Map.get(preparation, :sections_to_render, [])
|
||||
task_names = Enum.map(sections, &apply_validation_section_task_name/1)
|
||||
|
||||
Enum.each(sections, fn section ->
|
||||
task_name = apply_validation_section_task_name(section)
|
||||
|
||||
{:ok, _task} =
|
||||
Tasks.submit_task(
|
||||
task_name,
|
||||
fn report ->
|
||||
{:ok, _result} =
|
||||
Generation.apply_validation_section(project.id, validation_report, section,
|
||||
on_progress: report
|
||||
)
|
||||
|
||||
report.(1.0, apply_validation_section_complete_message(section))
|
||||
:ok
|
||||
end,
|
||||
attrs
|
||||
)
|
||||
end)
|
||||
|
||||
case task_names do
|
||||
[] -> :ok
|
||||
names -> wait_for_group_phase(group_id, names, @rebuild_phase_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
defp run_apply_validation_refresh_task(project, group_id, attrs, kind) do
|
||||
task_name = apply_validation_refresh_task_name(kind)
|
||||
|
||||
{:ok, _task} =
|
||||
Tasks.submit_task(
|
||||
task_name,
|
||||
fn report ->
|
||||
{:ok, _result} =
|
||||
Generation.refresh_validation_ancillary_outputs(project.id, kind, on_progress: report)
|
||||
|
||||
report.(1.0, apply_validation_refresh_complete_message(kind))
|
||||
:ok
|
||||
end,
|
||||
attrs
|
||||
)
|
||||
|
||||
wait_for_group_phase(group_id, [task_name], @rebuild_phase_timeout)
|
||||
end
|
||||
|
||||
defp submit_apply_validation_check_task(project, attrs) do
|
||||
{:ok, _task} =
|
||||
Tasks.submit_task(
|
||||
"Validate Site",
|
||||
fn report ->
|
||||
{:ok, validation} =
|
||||
Generation.validate_site(project.id, @site_sections, on_progress: report)
|
||||
|
||||
site_validation_result(project.id, validation)
|
||||
end,
|
||||
attrs
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp completed_task_result(task_id) do
|
||||
case Tasks.get_task(task_id) do
|
||||
%{status: :completed, result: result} when is_map(result) -> {:ok, result}
|
||||
%{status: :failed, error: error} -> {:error, error}
|
||||
_other -> {:error, :task_result_unavailable}
|
||||
end
|
||||
end
|
||||
|
||||
defp validation_apply_changed?(preparation) do
|
||||
Map.get(preparation, :sections_to_render, []) != [] or
|
||||
Map.get(preparation, :deleted_url_count, 0) > 0
|
||||
end
|
||||
|
||||
defp apply_validation_section_task_name(:core), do: "Render Site Core"
|
||||
defp apply_validation_section_task_name(:single), do: "Render Single Posts"
|
||||
defp apply_validation_section_task_name(:category), do: "Render Category Archives"
|
||||
defp apply_validation_section_task_name(:tag), do: "Render Tag Archives"
|
||||
defp apply_validation_section_task_name(:date), do: "Render Date Archives"
|
||||
|
||||
defp apply_validation_section_complete_message(:core), do: "Render Site Core complete"
|
||||
defp apply_validation_section_complete_message(:single), do: "Render Single Posts complete"
|
||||
|
||||
defp apply_validation_section_complete_message(:category),
|
||||
do: "Render Category Archives complete"
|
||||
|
||||
defp apply_validation_section_complete_message(:tag), do: "Render Tag Archives complete"
|
||||
defp apply_validation_section_complete_message(:date), do: "Render Date Archives complete"
|
||||
|
||||
defp apply_validation_refresh_task_name(:calendar), do: "Regenerate Calendar"
|
||||
|
||||
defp apply_validation_refresh_complete_message(:calendar), do: "Regenerate Calendar complete"
|
||||
|
||||
defp translation_fill_enabled?(metadata) do
|
||||
([metadata.main_language] ++ metadata.blog_languages)
|
||||
|> Enum.map(fn language ->
|
||||
|
||||
@@ -21,6 +21,7 @@ defmodule BDS.Generation do
|
||||
import BDS.Generation.Progress
|
||||
import BDS.Generation.Outputs
|
||||
import BDS.Generation.Data
|
||||
import BDS.Generation.Renderers, only: [build_list_posts: 3]
|
||||
import BDS.Generation.Validation
|
||||
|
||||
alias BDS.Generation.GeneratedFileHash
|
||||
@@ -227,62 +228,127 @@ defmodule BDS.Generation do
|
||||
apply_validation(project_id, report, [])
|
||||
end
|
||||
|
||||
@spec prepare_validation_apply(String.t(), map(), generation_opts()) ::
|
||||
{:ok, map()} | {:error, term()}
|
||||
def prepare_validation_apply(project_id, report, opts \\ [])
|
||||
|
||||
def prepare_validation_apply(project_id, report, opts)
|
||||
when is_binary(project_id) and is_map(report) and is_list(opts) do
|
||||
on_progress = callback(opts)
|
||||
:ok = report_validation_progress(on_progress, 0.0, "Planning validation apply steps...")
|
||||
|
||||
with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do
|
||||
sections_to_render = validation_apply_sections_to_render(apply_plan.targeted_plan)
|
||||
project = Projects.get_project!(project_id)
|
||||
|
||||
:ok = report_validation_progress(on_progress, 0.2, "Deleting extra URLs...")
|
||||
|
||||
{deleted_url_count, removed_empty_dir_count} =
|
||||
delete_extra_validation_paths(
|
||||
project_id,
|
||||
project,
|
||||
Map.get(report, :extra_url_paths, []),
|
||||
on_progress
|
||||
)
|
||||
|
||||
:ok =
|
||||
report_validation_progress(
|
||||
on_progress,
|
||||
1.0,
|
||||
"Validation apply prepared"
|
||||
)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
sections_to_render: sections_to_render,
|
||||
deleted_url_count: deleted_url_count,
|
||||
removed_empty_dir_count: removed_empty_dir_count
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec apply_validation_section(String.t(), map(), section(), generation_opts()) ::
|
||||
{:ok, map()} | {:error, term()}
|
||||
def apply_validation_section(project_id, report, section, opts \\ [])
|
||||
|
||||
def apply_validation_section(project_id, report, section, opts)
|
||||
when is_binary(project_id) and is_map(report) and section in @core_sections and
|
||||
is_list(opts) do
|
||||
on_progress = callback(opts)
|
||||
:ok = report_validation_progress(on_progress, 0.0, validation_section_task_label(section))
|
||||
|
||||
with {:ok, apply_plan} <- validation_apply_plan(project_id, [section], report) do
|
||||
outputs_to_render =
|
||||
targeted_apply_outputs(apply_plan.plan, apply_plan.targeted_plan, [section])
|
||||
|
||||
rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress)
|
||||
|
||||
{:ok, %{section: section, rendered_url_count: rendered_url_count}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refresh_validation_ancillary_outputs(String.t(), :calendar, generation_opts()) ::
|
||||
{:ok, map()} | {:error, term()}
|
||||
def refresh_validation_ancillary_outputs(project_id, kind, opts \\ [])
|
||||
|
||||
def refresh_validation_ancillary_outputs(project_id, kind, opts)
|
||||
when is_binary(project_id) and kind in [:calendar] and is_list(opts) do
|
||||
on_progress = callback(opts)
|
||||
:ok = report_validation_progress(on_progress, 0.0, ancillary_validation_task_label(kind))
|
||||
|
||||
with {:ok, plan} <- plan_generation(project_id, @core_sections) do
|
||||
expected_output_map = refresh_validation_output_map(plan, kind, on_progress)
|
||||
paths = ancillary_validation_paths(expected_output_map, kind)
|
||||
label = ancillary_validation_progress_label(kind)
|
||||
total = length(paths)
|
||||
|
||||
:ok = report_generation_started(on_progress, total, label)
|
||||
|
||||
paths
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.each(fn {relative_path, index} ->
|
||||
_ =
|
||||
write_generated_file(
|
||||
project_id,
|
||||
relative_path,
|
||||
Map.fetch!(expected_output_map, relative_path)
|
||||
)
|
||||
|
||||
:ok = report_generation_progress(on_progress, index, total, label)
|
||||
end)
|
||||
|
||||
{:ok, %{kind: kind, written_count: length(paths)}}
|
||||
end
|
||||
end
|
||||
|
||||
defp refresh_validation_output_map(plan, :calendar, _on_progress) do
|
||||
data = generation_data(plan)
|
||||
%{"calendar.json" => BDS.Generation.Sitemap.render_calendar(data.published_list_posts)}
|
||||
end
|
||||
|
||||
@spec apply_validation(String.t(), map(), generation_opts()) :: {:ok, map()} | {:error, term()}
|
||||
def apply_validation(project_id, report, opts)
|
||||
when is_binary(project_id) and is_map(report) and is_list(opts) do
|
||||
on_progress = callback(opts)
|
||||
|
||||
with {:ok, plan} <- plan_generation(project_id, @core_sections) do
|
||||
expected_outputs = build_outputs(plan)
|
||||
expected_output_map = Map.new(expected_outputs)
|
||||
with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do
|
||||
plan = apply_plan.plan
|
||||
project = Projects.get_project!(project_id)
|
||||
published_posts = list_published_posts(project_id)
|
||||
|
||||
targeted_plan =
|
||||
build_targeted_validation_plan(
|
||||
plan_validation_paths(report_paths(report), additional_languages(plan)),
|
||||
published_posts
|
||||
)
|
||||
outputs_to_render = targeted_apply_outputs(plan, apply_plan.targeted_plan, plan.sections)
|
||||
|
||||
outputs_to_render =
|
||||
expected_outputs
|
||||
|> Enum.filter(fn {relative_path, _content} ->
|
||||
targeted_output?(
|
||||
relative_path,
|
||||
targeted_plan,
|
||||
plan.language,
|
||||
additional_languages(plan)
|
||||
)
|
||||
end)
|
||||
|
||||
total_to_render = length(outputs_to_render)
|
||||
:ok = report_generation_started(on_progress, total_to_render, "validation routes")
|
||||
|
||||
outputs_to_render
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.each(fn {{relative_path, content}, index} ->
|
||||
_ =
|
||||
write_generated_file(project_id, relative_path, content,
|
||||
refresh_timestamp_on_unchanged: route_html_path?(relative_path)
|
||||
)
|
||||
|
||||
:ok =
|
||||
report_generation_progress(on_progress, index, total_to_render, "validation routes")
|
||||
end)
|
||||
rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress)
|
||||
|
||||
{deleted_url_count, removed_empty_dir_count} =
|
||||
delete_extra_validation_paths(project_id, project, Map.get(report, :extra_url_paths, []))
|
||||
|
||||
if outputs_to_render != [] or deleted_url_count > 0 do
|
||||
write_ancillary_validation_outputs(project_id, expected_output_map)
|
||||
{:ok, _} = refresh_validation_ancillary_outputs(project_id, :calendar)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
rendered_url_count:
|
||||
Enum.count(outputs_to_render, fn {relative_path, _content} ->
|
||||
route_html_path?(relative_path)
|
||||
end),
|
||||
rendered_url_count: rendered_url_count,
|
||||
deleted_url_count: deleted_url_count,
|
||||
removed_empty_dir_count: removed_empty_dir_count
|
||||
}}
|
||||
@@ -372,7 +438,7 @@ defmodule BDS.Generation do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp build_outputs(plan) do
|
||||
defp build_outputs(plan, on_output \\ nil) do
|
||||
data = generation_data(plan)
|
||||
published_translations = flattened_generation_translations(data.translations_by_post)
|
||||
translations_by_post_language = translation_lookup_map(published_translations)
|
||||
@@ -419,7 +485,8 @@ defmodule BDS.Generation do
|
||||
build_core_outputs(
|
||||
plan,
|
||||
data.published_list_posts,
|
||||
localized_list_posts_by_language
|
||||
localized_list_posts_by_language,
|
||||
on_output
|
||||
)
|
||||
else
|
||||
[]
|
||||
@@ -432,7 +499,8 @@ defmodule BDS.Generation do
|
||||
plan.language,
|
||||
data.published_posts,
|
||||
translations_by_post_language,
|
||||
localized_posts_by_language
|
||||
localized_posts_by_language,
|
||||
on_output
|
||||
)
|
||||
else
|
||||
[]
|
||||
@@ -445,14 +513,15 @@ defmodule BDS.Generation do
|
||||
plan.language,
|
||||
data.published_posts,
|
||||
translations_by_post_language,
|
||||
localized_posts_by_language
|
||||
localized_posts_by_language,
|
||||
on_output
|
||||
)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
archive_outputs =
|
||||
build_archive_outputs(plan, data.post_index, localized_post_indexes)
|
||||
build_archive_outputs(plan, data.post_index, localized_post_indexes, on_output)
|
||||
|
||||
urls =
|
||||
(core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs)
|
||||
@@ -606,6 +675,346 @@ defmodule BDS.Generation do
|
||||
end
|
||||
end
|
||||
|
||||
defp validation_apply_plan(project_id, sections, report) do
|
||||
with {:ok, plan} <- plan_generation(project_id, sections) do
|
||||
published_posts = list_published_posts(project_id)
|
||||
|
||||
targeted_plan =
|
||||
build_targeted_validation_plan(
|
||||
plan_validation_paths(report_paths(report), additional_languages(plan)),
|
||||
published_posts
|
||||
)
|
||||
|
||||
{:ok, %{plan: plan, targeted_plan: targeted_plan}}
|
||||
end
|
||||
end
|
||||
|
||||
# Render ONLY the routes targeted by the validation report for the given
|
||||
# sections, matching the old app's behaviour of filtering the render inputs
|
||||
# rather than rendering the whole site and discarding the surplus. Falls back
|
||||
# to a full section render only when a report path could not be classified.
|
||||
defp targeted_apply_outputs(plan, targeted_plan, sections) do
|
||||
if fallback_validation_section_render?(targeted_plan) do
|
||||
Enum.filter(build_outputs(plan), fn {relative_path, _content} ->
|
||||
path_section(relative_path) in sections
|
||||
end)
|
||||
else
|
||||
render_data = validation_render_data(plan)
|
||||
|
||||
Enum.flat_map(sections, fn section ->
|
||||
build_targeted_section_outputs(plan, render_data, targeted_plan, section)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
# Write the targeted outputs, streaming one progress message per rewritten URL
|
||||
# (exactly like the old app's `Generated /url` reporting) so the task shows the
|
||||
# actual URLs and advances by the number of URLs rewritten.
|
||||
defp write_validation_outputs(project_id, outputs, on_progress) do
|
||||
route_total = Enum.count(outputs, fn {relative_path, _content} -> route_html_path?(relative_path) end)
|
||||
|
||||
:ok = report_validation_progress(on_progress, 0.0, validation_rewrite_started_message(route_total))
|
||||
|
||||
{rendered, _total} =
|
||||
Enum.reduce(outputs, {0, route_total}, fn {relative_path, content}, {rendered, total} ->
|
||||
_ =
|
||||
write_generated_file(project_id, relative_path, content,
|
||||
refresh_timestamp_on_unchanged: route_html_path?(relative_path)
|
||||
)
|
||||
|
||||
if route_html_path?(relative_path) do
|
||||
rewritten = rendered + 1
|
||||
progress = if total > 0, do: rewritten / total, else: 1.0
|
||||
|
||||
:ok =
|
||||
report_validation_progress(
|
||||
on_progress,
|
||||
progress,
|
||||
"Rewrote #{relative_path_to_url_path(relative_path)} (#{rewritten}/#{total})"
|
||||
)
|
||||
|
||||
{rewritten, total}
|
||||
else
|
||||
{rendered, total}
|
||||
end
|
||||
end)
|
||||
|
||||
rendered
|
||||
end
|
||||
|
||||
defp validation_rewrite_started_message(0), do: "No URLs to rewrite"
|
||||
defp validation_rewrite_started_message(1), do: "Rewriting 1 URL..."
|
||||
defp validation_rewrite_started_message(total), do: "Rewriting #{total} URLs..."
|
||||
|
||||
defp validation_render_data(plan) do
|
||||
data = generation_data(plan)
|
||||
|
||||
translations_by_post_language =
|
||||
data.translations_by_post
|
||||
|> flattened_generation_translations()
|
||||
|> translation_lookup_map()
|
||||
|
||||
translatable_posts =
|
||||
Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
||||
|
||||
translatable_list_posts =
|
||||
Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
||||
|
||||
localized_posts_by_language =
|
||||
Map.new(additional_languages(plan), fn language ->
|
||||
{language,
|
||||
resolve_posts_for_language(
|
||||
translatable_posts,
|
||||
language,
|
||||
translations_by_post_language,
|
||||
plan.language
|
||||
)}
|
||||
end)
|
||||
|
||||
localized_list_posts_by_language =
|
||||
Map.new(additional_languages(plan), fn language ->
|
||||
{language,
|
||||
resolve_posts_for_language(
|
||||
translatable_list_posts,
|
||||
language,
|
||||
translations_by_post_language,
|
||||
plan.language
|
||||
)}
|
||||
end)
|
||||
|
||||
localized_post_indexes =
|
||||
Map.new(localized_list_posts_by_language, fn {language, posts} ->
|
||||
{language, build_generation_post_index(posts)}
|
||||
end)
|
||||
|
||||
%{
|
||||
data: data,
|
||||
translations_by_post_language: translations_by_post_language,
|
||||
localized_posts_by_language: localized_posts_by_language,
|
||||
localized_list_posts_by_language: localized_list_posts_by_language,
|
||||
localized_post_indexes: localized_post_indexes
|
||||
}
|
||||
end
|
||||
|
||||
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :core) do
|
||||
data = render_data.data
|
||||
|
||||
main_root =
|
||||
if targeted_plan.request_root_routes do
|
||||
posts = build_list_posts(plan.base_url, data.published_list_posts, nil)
|
||||
build_root_outputs(plan, plan.language, posts)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
language_root =
|
||||
Enum.flat_map(additional_languages(plan), fn language ->
|
||||
language_plan = language_targeted_plan(targeted_plan, language)
|
||||
|
||||
if language_plan.request_root_routes do
|
||||
source = Map.get(render_data.localized_list_posts_by_language, language, [])
|
||||
prefix = route_language(plan.language, language)
|
||||
posts = build_list_posts(plan.base_url, source, prefix)
|
||||
build_root_outputs(plan, language, posts)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
|
||||
main_page_posts =
|
||||
Enum.filter(
|
||||
data.published_posts,
|
||||
&MapSet.member?(targeted_plan.requested_page_slugs, &1.slug)
|
||||
)
|
||||
|
||||
localized_page_posts =
|
||||
Map.new(additional_languages(plan), fn language ->
|
||||
language_plan = language_targeted_plan(targeted_plan, language)
|
||||
source = Map.get(render_data.localized_posts_by_language, language, [])
|
||||
{language, Enum.filter(source, &MapSet.member?(language_plan.requested_page_slugs, &1.slug))}
|
||||
end)
|
||||
|
||||
page_outputs =
|
||||
build_page_outputs(
|
||||
plan.project_id,
|
||||
plan.language,
|
||||
main_page_posts,
|
||||
render_data.translations_by_post_language,
|
||||
localized_page_posts
|
||||
)
|
||||
|
||||
main_root ++ language_root ++ page_outputs
|
||||
end
|
||||
|
||||
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :single) do
|
||||
data = render_data.data
|
||||
|
||||
main_posts =
|
||||
filter_route_posts(data.published_posts, targeted_plan.requested_post_routes)
|
||||
|
||||
localized_posts =
|
||||
Map.new(additional_languages(plan), fn language ->
|
||||
language_plan = language_targeted_plan(targeted_plan, language)
|
||||
source = Map.get(render_data.localized_posts_by_language, language, [])
|
||||
{language, filter_route_posts(source, language_plan.requested_post_routes)}
|
||||
end)
|
||||
|
||||
build_single_outputs(
|
||||
plan.project_id,
|
||||
plan.language,
|
||||
main_posts,
|
||||
render_data.translations_by_post_language,
|
||||
localized_posts
|
||||
)
|
||||
end
|
||||
|
||||
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :category) do
|
||||
build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg,
|
||||
index,
|
||||
language_plan,
|
||||
languages ->
|
||||
build_category_outputs(
|
||||
plan_arg,
|
||||
filter_collection_by_segment(index.posts_by_category, language_plan.requested_category_slugs),
|
||||
languages
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :tag) do
|
||||
build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg,
|
||||
index,
|
||||
language_plan,
|
||||
languages ->
|
||||
build_tag_outputs(
|
||||
plan_arg,
|
||||
filter_collection_by_segment(index.posts_by_tag, language_plan.requested_tag_slugs),
|
||||
languages
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :date) do
|
||||
build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg,
|
||||
index,
|
||||
language_plan,
|
||||
languages ->
|
||||
build_date_outputs(plan_arg, filter_date_index(index, language_plan), languages)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_targeted_archive_outputs(plan, render_data, targeted_plan, builder) do
|
||||
main_outputs = builder.(plan, render_data.data.post_index, targeted_plan, [plan.language])
|
||||
|
||||
language_outputs =
|
||||
Enum.flat_map(additional_languages(plan), fn language ->
|
||||
language_plan = language_targeted_plan(targeted_plan, language)
|
||||
index = Map.get(render_data.localized_post_indexes, language, empty_generation_post_index())
|
||||
builder.(plan, index, language_plan, [language])
|
||||
end)
|
||||
|
||||
main_outputs ++ language_outputs
|
||||
end
|
||||
|
||||
defp language_targeted_plan(targeted_plan, language) do
|
||||
Map.get(targeted_plan.language_plans, language, empty_validation_path_plan())
|
||||
end
|
||||
|
||||
defp filter_route_posts(posts, route_keys) do
|
||||
Enum.filter(posts, fn post ->
|
||||
{year, month, day} = local_date_parts!(post.created_at)
|
||||
MapSet.member?(route_keys, route_key(year, month, day, post.slug))
|
||||
end)
|
||||
end
|
||||
|
||||
defp filter_collection_by_segment(collection, segment_slugs) do
|
||||
collection
|
||||
|> Enum.filter(fn {key, _posts} ->
|
||||
MapSet.member?(segment_slugs, archive_route_segment(key))
|
||||
end)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp filter_date_index(index, language_plan) do
|
||||
%{
|
||||
posts_by_year: filter_map_by_keys(index.posts_by_year, language_plan.requested_years),
|
||||
posts_by_year_month:
|
||||
filter_map_by_keys(index.posts_by_year_month, language_plan.requested_year_months),
|
||||
posts_by_year_month_day:
|
||||
filter_map_by_keys(index.posts_by_year_month_day, language_plan.requested_year_month_days)
|
||||
}
|
||||
end
|
||||
|
||||
defp filter_map_by_keys(map, keys) do
|
||||
map
|
||||
|> Enum.filter(fn {key, _value} -> MapSet.member?(keys, key) end)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp empty_generation_post_index do
|
||||
%{
|
||||
posts_by_category: %{},
|
||||
posts_by_tag: %{},
|
||||
posts_by_year: %{},
|
||||
posts_by_year_month: %{},
|
||||
posts_by_year_month_day: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp validation_apply_sections_to_render(targeted_plan) do
|
||||
if fallback_validation_section_render?(targeted_plan) do
|
||||
[:category, :tag, :date, :core, :single]
|
||||
else
|
||||
targeted_plan
|
||||
|> validation_apply_section_plans()
|
||||
|> Enum.reduce([], &append_validation_apply_sections/2)
|
||||
end
|
||||
end
|
||||
|
||||
defp fallback_validation_section_render?(%{requires_fallback_section_render: true}), do: true
|
||||
|
||||
defp fallback_validation_section_render?(%{language_plans: language_plans}) do
|
||||
Enum.any?(Map.values(language_plans), &fallback_validation_section_render?/1)
|
||||
end
|
||||
|
||||
defp validation_apply_section_plans(targeted_plan) do
|
||||
[targeted_plan | Map.values(Map.get(targeted_plan, :language_plans, %{}))]
|
||||
end
|
||||
|
||||
defp append_validation_apply_sections(plan, sections) do
|
||||
sections
|
||||
|> maybe_append_validation_apply_section(:core, validation_apply_core_section?(plan))
|
||||
|> maybe_append_validation_apply_section(
|
||||
:single,
|
||||
not MapSet.equal?(plan.requested_post_routes, MapSet.new())
|
||||
)
|
||||
|> maybe_append_validation_apply_section(
|
||||
:category,
|
||||
not MapSet.equal?(plan.requested_category_slugs, MapSet.new())
|
||||
)
|
||||
|> maybe_append_validation_apply_section(
|
||||
:tag,
|
||||
not MapSet.equal?(plan.requested_tag_slugs, MapSet.new())
|
||||
)
|
||||
|> maybe_append_validation_apply_section(:date, validation_apply_date_section?(plan))
|
||||
end
|
||||
|
||||
defp validation_apply_core_section?(plan) do
|
||||
plan.request_root_routes or not MapSet.equal?(plan.requested_page_slugs, MapSet.new())
|
||||
end
|
||||
|
||||
defp validation_apply_date_section?(plan) do
|
||||
not MapSet.equal?(plan.requested_years, MapSet.new()) or
|
||||
not MapSet.equal?(plan.requested_year_months, MapSet.new()) or
|
||||
not MapSet.equal?(plan.requested_year_month_days, MapSet.new())
|
||||
end
|
||||
|
||||
defp maybe_append_validation_apply_section(sections, _section, false), do: sections
|
||||
|
||||
defp maybe_append_validation_apply_section(sections, section, true) do
|
||||
if section in sections, do: sections, else: sections ++ [section]
|
||||
end
|
||||
|
||||
defp path_section(relative_path) do
|
||||
segments = String.split(relative_path, "/", trim: true)
|
||||
|
||||
@@ -698,50 +1107,70 @@ defmodule BDS.Generation do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp validation_section_task_label(:core), do: "Render Site Core"
|
||||
defp validation_section_task_label(:single), do: "Render Single Posts"
|
||||
defp validation_section_task_label(:category), do: "Render Category Archives"
|
||||
defp validation_section_task_label(:tag), do: "Render Tag Archives"
|
||||
defp validation_section_task_label(:date), do: "Render Date Archives"
|
||||
|
||||
defp ancillary_validation_progress_label(:calendar), do: "calendar files"
|
||||
|
||||
defp ancillary_validation_task_label(:calendar), do: "Regenerate Calendar"
|
||||
|
||||
defp delete_extra_validation_paths(project_id, project, extra_url_paths) do
|
||||
Enum.reduce(extra_url_paths, {0, 0}, fn url_path, {deleted_count, removed_dir_count} ->
|
||||
delete_extra_validation_paths(project_id, project, extra_url_paths, nil)
|
||||
end
|
||||
|
||||
defp delete_extra_validation_paths(project_id, project, extra_url_paths, on_progress) do
|
||||
total = length(extra_url_paths)
|
||||
|
||||
if total == 0 do
|
||||
:ok = report_validation_progress(on_progress, 0.5, "No extra URLs to delete")
|
||||
end
|
||||
|
||||
extra_url_paths
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.reduce({0, 0}, fn {url_path, index}, {deleted_count, removed_dir_count} ->
|
||||
relative_path = url_path_to_relative_index_path(url_path)
|
||||
full_path = output_path(project, relative_path)
|
||||
|
||||
case File.rm(full_path) do
|
||||
:ok ->
|
||||
Repo.delete_all(
|
||||
from generated_file in GeneratedFileHash,
|
||||
where:
|
||||
generated_file.project_id == ^project_id and
|
||||
generated_file.relative_path == ^relative_path
|
||||
)
|
||||
result =
|
||||
case File.rm(full_path) do
|
||||
:ok ->
|
||||
Repo.delete_all(
|
||||
from generated_file in GeneratedFileHash,
|
||||
where:
|
||||
generated_file.project_id == ^project_id and
|
||||
generated_file.relative_path == ^relative_path
|
||||
)
|
||||
|
||||
{pruned_count, _last_dir} =
|
||||
prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, ""))
|
||||
{pruned_count, _last_dir} =
|
||||
prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, ""))
|
||||
|
||||
{deleted_count + 1, removed_dir_count + pruned_count}
|
||||
{deleted_count + 1, removed_dir_count + pruned_count}
|
||||
|
||||
{:error, :enoent} ->
|
||||
{deleted_count, removed_dir_count}
|
||||
{:error, :enoent} ->
|
||||
{deleted_count, removed_dir_count}
|
||||
|
||||
{:error, _reason} ->
|
||||
{deleted_count, removed_dir_count}
|
||||
end
|
||||
{:error, _reason} ->
|
||||
{deleted_count, removed_dir_count}
|
||||
end
|
||||
|
||||
:ok =
|
||||
report_validation_progress(
|
||||
on_progress,
|
||||
0.2 + 0.3 * index / max(total, 1),
|
||||
"Deleted #{index}/#{total} extra URLs"
|
||||
)
|
||||
|
||||
result
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_ancillary_validation_outputs(project_id, expected_output_map) do
|
||||
ancillary_paths =
|
||||
Enum.filter(Map.keys(expected_output_map), fn relative_path ->
|
||||
relative_path == "calendar.json" or String.contains?(relative_path, "pagefind/")
|
||||
end)
|
||||
|
||||
Enum.each(ancillary_paths, fn relative_path ->
|
||||
_ =
|
||||
write_generated_file(
|
||||
project_id,
|
||||
relative_path,
|
||||
Map.fetch!(expected_output_map, relative_path)
|
||||
)
|
||||
end)
|
||||
|
||||
:ok
|
||||
defp ancillary_validation_paths(expected_output_map, :calendar) do
|
||||
expected_output_map
|
||||
|> Map.keys()
|
||||
|> Enum.filter(&(&1 == "calendar.json"))
|
||||
end
|
||||
|
||||
defp output_path(project, relative_path) do
|
||||
|
||||
@@ -7,6 +7,8 @@ defmodule BDS.Generation.Outputs do
|
||||
|
||||
alias BDS.Rendering.TemplateSelection
|
||||
|
||||
@type output_callback :: ({String.t(), iodata()} -> any()) | nil
|
||||
|
||||
@spec additional_languages(map()) :: [String.t()]
|
||||
def additional_languages(plan) do
|
||||
Enum.reject(plan.blog_languages, &(&1 == plan.language))
|
||||
@@ -149,16 +151,17 @@ defmodule BDS.Generation.Outputs do
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_archive_outputs(map(), map(), map()) :: [{String.t(), iodata()}]
|
||||
def build_archive_outputs(plan, post_index, localized_post_indexes) do
|
||||
@spec build_archive_outputs(map(), map(), map(), output_callback()) :: [{String.t(), iodata()}]
|
||||
def build_archive_outputs(plan, post_index, localized_post_indexes, on_output \\ nil) do
|
||||
category_outputs =
|
||||
if :category in plan.sections do
|
||||
build_category_outputs(plan, post_index.posts_by_category, [plan.language]) ++
|
||||
build_category_outputs(plan, post_index.posts_by_category, [plan.language], on_output) ++
|
||||
Enum.flat_map(additional_languages(plan), fn language ->
|
||||
build_category_outputs(
|
||||
plan,
|
||||
Map.get(localized_post_indexes, language, %{posts_by_category: %{}}).posts_by_category,
|
||||
[language]
|
||||
[language],
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
else
|
||||
@@ -167,12 +170,13 @@ defmodule BDS.Generation.Outputs do
|
||||
|
||||
tag_outputs =
|
||||
if :tag in plan.sections do
|
||||
build_tag_outputs(plan, post_index.posts_by_tag, [plan.language]) ++
|
||||
build_tag_outputs(plan, post_index.posts_by_tag, [plan.language], on_output) ++
|
||||
Enum.flat_map(additional_languages(plan), fn language ->
|
||||
build_tag_outputs(
|
||||
plan,
|
||||
Map.get(localized_post_indexes, language, %{posts_by_tag: %{}}).posts_by_tag,
|
||||
[language]
|
||||
[language],
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
else
|
||||
@@ -181,7 +185,7 @@ defmodule BDS.Generation.Outputs do
|
||||
|
||||
date_outputs =
|
||||
if :date in plan.sections do
|
||||
build_date_outputs(plan, post_index, [plan.language]) ++
|
||||
build_date_outputs(plan, post_index, [plan.language], on_output) ++
|
||||
Enum.flat_map(additional_languages(plan), fn language ->
|
||||
build_date_outputs(
|
||||
plan,
|
||||
@@ -190,7 +194,8 @@ defmodule BDS.Generation.Outputs do
|
||||
language,
|
||||
%{posts_by_year: %{}, posts_by_year_month: %{}, posts_by_year_month_day: %{}}
|
||||
),
|
||||
[language]
|
||||
[language],
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
else
|
||||
@@ -200,8 +205,10 @@ defmodule BDS.Generation.Outputs do
|
||||
category_outputs ++ tag_outputs ++ date_outputs
|
||||
end
|
||||
|
||||
@spec build_category_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}]
|
||||
def build_category_outputs(plan, posts_by_category, languages) do
|
||||
@spec build_category_outputs(map(), map(), [String.t()], output_callback()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_category_outputs(plan, posts_by_category, languages, on_output \\ nil) do
|
||||
Enum.flat_map(posts_by_category, fn {category, posts} ->
|
||||
paginated_posts = Enum.chunk_every(posts, max(plan.max_posts_per_page, 1))
|
||||
category_slug = archive_route_segment(category)
|
||||
@@ -240,34 +247,46 @@ defmodule BDS.Generation.Outputs do
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
archive_path(
|
||||
route_language(plan.language, language),
|
||||
["category", category_slug],
|
||||
page_number
|
||||
),
|
||||
render_archive_page(plan, category, page_posts, language, "category", pagination)
|
||||
}
|
||||
output =
|
||||
{
|
||||
archive_path(
|
||||
route_language(plan.language, language),
|
||||
["category", category_slug],
|
||||
page_number
|
||||
),
|
||||
render_archive_page(plan, category, page_posts, language, "category", pagination)
|
||||
}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_tag_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}]
|
||||
def build_tag_outputs(plan, posts_by_tag, languages) do
|
||||
@spec build_tag_outputs(map(), map(), [String.t()], output_callback()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_tag_outputs(plan, posts_by_tag, languages, on_output \\ nil) do
|
||||
Enum.flat_map(posts_by_tag, fn {tag, posts} ->
|
||||
tag_slug = archive_route_segment(tag)
|
||||
|
||||
build_paginated_archive_outputs(plan, languages, ["tag", tag_slug], posts, fn page_posts,
|
||||
language,
|
||||
pagination ->
|
||||
render_archive_page(plan, tag, page_posts, language, "tag", pagination)
|
||||
end)
|
||||
build_paginated_archive_outputs(
|
||||
plan,
|
||||
languages,
|
||||
["tag", tag_slug],
|
||||
posts,
|
||||
fn page_posts, language, pagination ->
|
||||
render_archive_page(plan, tag, page_posts, language, "tag", pagination)
|
||||
end,
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_date_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}]
|
||||
def build_date_outputs(plan, post_index, languages) do
|
||||
@spec build_date_outputs(map(), map(), [String.t()], output_callback()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_date_outputs(plan, post_index, languages, on_output \\ nil) do
|
||||
year_outputs =
|
||||
Enum.flat_map(post_index.posts_by_year, fn {year, posts} ->
|
||||
build_paginated_archive_outputs(
|
||||
@@ -284,7 +303,8 @@ defmodule BDS.Generation.Outputs do
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
end
|
||||
end,
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
|
||||
@@ -292,59 +312,73 @@ defmodule BDS.Generation.Outputs do
|
||||
Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
|
||||
[year, month] = String.split(year_month, "/", parts: 2)
|
||||
|
||||
build_paginated_archive_outputs(plan, languages, [year, month], posts, fn page_posts,
|
||||
language,
|
||||
pagination ->
|
||||
render_date_archive_page(
|
||||
plan,
|
||||
"#{year}-#{month}",
|
||||
%{kind: "month", year: String.to_integer(year), month: String.to_integer(month)},
|
||||
page_posts,
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
end)
|
||||
build_paginated_archive_outputs(
|
||||
plan,
|
||||
languages,
|
||||
[year, month],
|
||||
posts,
|
||||
fn page_posts, language, pagination ->
|
||||
render_date_archive_page(
|
||||
plan,
|
||||
"#{year}-#{month}",
|
||||
%{kind: "month", year: String.to_integer(year), month: String.to_integer(month)},
|
||||
page_posts,
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
end,
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
|
||||
day_outputs =
|
||||
Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} ->
|
||||
[year, month, day] = String.split(year_month_day, "/", parts: 3)
|
||||
|
||||
build_paginated_archive_outputs(plan, languages, [year, month, day], posts, fn page_posts,
|
||||
language,
|
||||
pagination ->
|
||||
render_date_archive_page(
|
||||
plan,
|
||||
"#{year}-#{month}-#{day}",
|
||||
%{
|
||||
kind: "day",
|
||||
year: String.to_integer(year),
|
||||
month: String.to_integer(month),
|
||||
day: String.to_integer(day)
|
||||
},
|
||||
page_posts,
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
end)
|
||||
build_paginated_archive_outputs(
|
||||
plan,
|
||||
languages,
|
||||
[year, month, day],
|
||||
posts,
|
||||
fn page_posts, language, pagination ->
|
||||
render_date_archive_page(
|
||||
plan,
|
||||
"#{year}-#{month}-#{day}",
|
||||
%{
|
||||
kind: "day",
|
||||
year: String.to_integer(year),
|
||||
month: String.to_integer(month),
|
||||
day: String.to_integer(day)
|
||||
},
|
||||
page_posts,
|
||||
language,
|
||||
pagination
|
||||
)
|
||||
end,
|
||||
on_output
|
||||
)
|
||||
end)
|
||||
|
||||
year_outputs ++ month_outputs ++ day_outputs
|
||||
end
|
||||
|
||||
@spec build_core_outputs(map(), [map()], map()) :: [{String.t(), iodata()}]
|
||||
def build_core_outputs(plan, published_posts, localized_posts_by_language) do
|
||||
@spec build_core_outputs(map(), [map()], map(), output_callback()) :: [{String.t(), iodata()}]
|
||||
def build_core_outputs(plan, published_posts, localized_posts_by_language, on_output \\ nil) do
|
||||
language = plan.language
|
||||
additional_languages = Enum.reject(plan.blog_languages, &(&1 == language))
|
||||
main_posts = build_list_posts(plan.base_url, published_posts, nil)
|
||||
|
||||
build_root_outputs(plan, language, main_posts) ++
|
||||
main_static_outputs =
|
||||
[
|
||||
{"404.html", render_not_found_output(plan, language)},
|
||||
{"feed.xml", render_feed(plan, language, published_posts)},
|
||||
{"atom.xml", render_atom(plan, language, published_posts)},
|
||||
{"calendar.json", render_calendar(published_posts)}
|
||||
] ++
|
||||
]
|
||||
|> Enum.map(&report_output(&1, on_output))
|
||||
|
||||
build_root_outputs(plan, language, main_posts, on_output) ++
|
||||
main_static_outputs ++
|
||||
Enum.flat_map(additional_languages, fn localized_language ->
|
||||
localized_prefix = route_language(plan.language, localized_language)
|
||||
localized_source_posts = Map.get(localized_posts_by_language, localized_language, [])
|
||||
@@ -352,19 +386,20 @@ defmodule BDS.Generation.Outputs do
|
||||
localized_posts =
|
||||
build_list_posts(plan.base_url, localized_source_posts, localized_prefix)
|
||||
|
||||
build_root_outputs(plan, localized_language, localized_posts) ++
|
||||
[
|
||||
{Path.join(localized_language, "404.html"),
|
||||
render_not_found_output(plan, localized_language)},
|
||||
{Path.join(localized_language, "feed.xml"),
|
||||
render_feed(plan, localized_language, localized_source_posts)},
|
||||
{Path.join(localized_language, "atom.xml"),
|
||||
render_atom(plan, localized_language, localized_source_posts)}
|
||||
]
|
||||
(build_root_outputs(plan, localized_language, localized_posts, on_output) ++
|
||||
[
|
||||
{Path.join(localized_language, "404.html"),
|
||||
render_not_found_output(plan, localized_language)},
|
||||
{Path.join(localized_language, "feed.xml"),
|
||||
render_feed(plan, localized_language, localized_source_posts)},
|
||||
{Path.join(localized_language, "atom.xml"),
|
||||
render_atom(plan, localized_language, localized_source_posts)}
|
||||
])
|
||||
|> Enum.map(&report_output(&1, on_output))
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map()) :: [
|
||||
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_page_outputs(
|
||||
@@ -372,39 +407,44 @@ defmodule BDS.Generation.Outputs do
|
||||
main_language,
|
||||
published_posts,
|
||||
translations_by_post_language,
|
||||
localized_posts_by_language
|
||||
localized_posts_by_language,
|
||||
on_output \\ nil
|
||||
) do
|
||||
page_outputs =
|
||||
published_posts
|
||||
|> Enum.filter(&("page" in (&1.categories || [])))
|
||||
|> Enum.map(fn post ->
|
||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||
render_language = effective_render_language(canonical_variant.language, main_language)
|
||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
|
||||
{page_output_path(post.slug, nil),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: canonical_variant.id,
|
||||
title: canonical_variant.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: canonical_variant.language,
|
||||
excerpt: canonical_variant.excerpt,
|
||||
_post_record: canonical_variant
|
||||
},
|
||||
fn ->
|
||||
render_post_page(
|
||||
canonical_variant.title,
|
||||
body,
|
||||
post.slug,
|
||||
canonical_variant.language
|
||||
)
|
||||
end
|
||||
)}
|
||||
output =
|
||||
{page_output_path(post.slug, nil),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: canonical_variant.id,
|
||||
title: canonical_variant.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: render_language,
|
||||
excerpt: canonical_variant.excerpt,
|
||||
_post_record: canonical_variant
|
||||
},
|
||||
fn ->
|
||||
render_post_page(
|
||||
canonical_variant.title,
|
||||
body,
|
||||
post.slug,
|
||||
render_language
|
||||
)
|
||||
end
|
||||
)}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
|
||||
translation_page_outputs =
|
||||
@@ -413,33 +453,39 @@ defmodule BDS.Generation.Outputs do
|
||||
posts
|
||||
|> Enum.filter(&("page" in (&1.categories || [])))
|
||||
|> Enum.map(fn post ->
|
||||
render_language = effective_render_language(post.language, language)
|
||||
body = load_body(project_id, post.file_path, post.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
|
||||
{page_output_path(post.slug, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: post.language,
|
||||
excerpt: post.excerpt,
|
||||
_post_record: post
|
||||
},
|
||||
fn -> render_post_page(post.title, body, post.slug, post.language) end
|
||||
)}
|
||||
output =
|
||||
{page_output_path(post.slug, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: render_language,
|
||||
excerpt: post.excerpt,
|
||||
_post_record: post
|
||||
},
|
||||
fn -> render_post_page(post.title, body, post.slug, render_language) end
|
||||
)}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
end)
|
||||
|
||||
page_outputs ++ translation_page_outputs
|
||||
end
|
||||
|
||||
@spec build_root_outputs(map(), String.t(), [map()]) :: [{String.t(), iodata()}]
|
||||
def build_root_outputs(plan, language, posts) do
|
||||
@spec build_root_outputs(map(), String.t(), [map()], output_callback()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_root_outputs(plan, language, posts, on_output \\ nil) do
|
||||
total_items = length(posts)
|
||||
total_pages = page_count(total_items, plan.max_posts_per_page)
|
||||
|
||||
@@ -449,30 +495,46 @@ defmodule BDS.Generation.Outputs do
|
||||
|> Enum.map(fn {page_posts, page_number} ->
|
||||
route_language = route_language(plan.language, language)
|
||||
|
||||
{root_output_path(route_language, page_number),
|
||||
render_list_output(
|
||||
plan,
|
||||
language,
|
||||
plan.project_name,
|
||||
page_posts,
|
||||
%{kind: "core"},
|
||||
pagination_for_page(
|
||||
page_number,
|
||||
total_pages,
|
||||
total_items,
|
||||
plan.max_posts_per_page,
|
||||
route_language,
|
||||
[]
|
||||
),
|
||||
fn -> render_home(plan, language) end
|
||||
)}
|
||||
output =
|
||||
{root_output_path(route_language, page_number),
|
||||
render_list_output(
|
||||
plan,
|
||||
language,
|
||||
plan.project_name,
|
||||
page_posts,
|
||||
%{kind: "core"},
|
||||
pagination_for_page(
|
||||
page_number,
|
||||
total_pages,
|
||||
total_items,
|
||||
plan.max_posts_per_page,
|
||||
route_language,
|
||||
[]
|
||||
),
|
||||
fn -> render_home(plan, language) end
|
||||
)}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_paginated_archive_outputs(map(), [String.t()], [String.t()], [map()], (... ->
|
||||
iodata())) ::
|
||||
@spec build_paginated_archive_outputs(
|
||||
map(),
|
||||
[String.t()],
|
||||
[String.t()],
|
||||
[map()],
|
||||
(... -> iodata()),
|
||||
output_callback()
|
||||
) ::
|
||||
[{String.t(), iodata()}]
|
||||
def build_paginated_archive_outputs(plan, languages, segments, posts, render_fun) do
|
||||
def build_paginated_archive_outputs(
|
||||
plan,
|
||||
languages,
|
||||
segments,
|
||||
posts,
|
||||
render_fun,
|
||||
on_output \\ nil
|
||||
) do
|
||||
total_items = length(posts)
|
||||
total_pages = page_count(total_items, plan.max_posts_per_page)
|
||||
|
||||
@@ -483,87 +545,100 @@ defmodule BDS.Generation.Outputs do
|
||||
Enum.map(languages, fn language ->
|
||||
route_language = route_language(plan.language, language)
|
||||
|
||||
{archive_path(route_language, segments, page_number),
|
||||
render_fun.(
|
||||
page_posts,
|
||||
language,
|
||||
pagination_for_page(
|
||||
page_number,
|
||||
total_pages,
|
||||
total_items,
|
||||
plan.max_posts_per_page,
|
||||
route_language,
|
||||
segments
|
||||
)
|
||||
)}
|
||||
output =
|
||||
{archive_path(route_language, segments, page_number),
|
||||
render_fun.(
|
||||
page_posts,
|
||||
language,
|
||||
pagination_for_page(
|
||||
page_number,
|
||||
total_pages,
|
||||
total_items,
|
||||
plan.max_posts_per_page,
|
||||
route_language,
|
||||
segments
|
||||
)
|
||||
)}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) ::
|
||||
[
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_single_outputs(
|
||||
project_id,
|
||||
main_language,
|
||||
published_posts,
|
||||
translations_by_post_language,
|
||||
localized_posts_by_language
|
||||
localized_posts_by_language,
|
||||
on_output \\ nil
|
||||
) do
|
||||
post_outputs =
|
||||
Enum.map(published_posts, fn post ->
|
||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||
render_language = effective_render_language(canonical_variant.language, main_language)
|
||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
|
||||
{post_output_path(post),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: canonical_variant.id,
|
||||
title: canonical_variant.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: canonical_variant.language,
|
||||
excerpt: canonical_variant.excerpt,
|
||||
_post_record: canonical_variant
|
||||
},
|
||||
fn ->
|
||||
render_post_page(
|
||||
canonical_variant.title,
|
||||
body,
|
||||
post.slug,
|
||||
canonical_variant.language
|
||||
)
|
||||
end
|
||||
)}
|
||||
output =
|
||||
{post_output_path(post),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: canonical_variant.id,
|
||||
title: canonical_variant.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: render_language,
|
||||
excerpt: canonical_variant.excerpt,
|
||||
_post_record: canonical_variant
|
||||
},
|
||||
fn ->
|
||||
render_post_page(
|
||||
canonical_variant.title,
|
||||
body,
|
||||
post.slug,
|
||||
render_language
|
||||
)
|
||||
end
|
||||
)}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
|
||||
translation_outputs =
|
||||
localized_posts_by_language
|
||||
|> Enum.flat_map(fn {language, posts} ->
|
||||
Enum.map(posts, fn post ->
|
||||
render_language = effective_render_language(post.language, language)
|
||||
body = load_body(project_id, post.file_path, post.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
|
||||
{post_output_path(post, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: post.language,
|
||||
excerpt: post.excerpt,
|
||||
_post_record: post
|
||||
},
|
||||
fn -> render_post_page(post.title, body, post.slug, post.language) end
|
||||
)}
|
||||
output =
|
||||
{post_output_path(post, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
effective_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: body,
|
||||
slug: post.slug,
|
||||
language: render_language,
|
||||
excerpt: post.excerpt,
|
||||
_post_record: post
|
||||
},
|
||||
fn -> render_post_page(post.title, body, post.slug, render_language) end
|
||||
)}
|
||||
|
||||
report_output(output, on_output)
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -583,4 +658,23 @@ defmodule BDS.Generation.Outputs do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp effective_render_language(language, _fallback_language)
|
||||
when is_binary(language) and language != "" do
|
||||
language
|
||||
end
|
||||
|
||||
defp effective_render_language(_language, fallback_language)
|
||||
when is_binary(fallback_language) and fallback_language != "" do
|
||||
fallback_language
|
||||
end
|
||||
|
||||
defp effective_render_language(_language, _fallback_language), do: "en"
|
||||
|
||||
defp report_output(output, nil), do: output
|
||||
|
||||
defp report_output(output, on_output) when is_function(on_output, 1) do
|
||||
on_output.(output)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -245,7 +245,9 @@ defmodule BDS.Generation.Validation do
|
||||
requested_tag_slugs: MapSet.new(),
|
||||
requested_years: MapSet.new(),
|
||||
requested_year_months: MapSet.new(),
|
||||
requested_year_month_days: MapSet.new(),
|
||||
requested_post_routes: [],
|
||||
requested_page_slugs: MapSet.new(),
|
||||
language_plans: %{}
|
||||
}
|
||||
end
|
||||
@@ -277,28 +279,38 @@ defmodule BDS.Generation.Validation do
|
||||
)
|
||||
|
||||
nil ->
|
||||
case Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) do
|
||||
[_, year, month] ->
|
||||
update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}"))
|
||||
|
||||
nil ->
|
||||
case Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path) do
|
||||
[_, year] ->
|
||||
update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year)))
|
||||
|
||||
nil ->
|
||||
if path == "/" or Regex.match?(~r|^/page/\d+$|, path) do
|
||||
%{plan | request_root_routes: true}
|
||||
else
|
||||
%{plan | requires_fallback_section_render: true}
|
||||
end
|
||||
end
|
||||
end
|
||||
classify_archive_or_page_validation_path(path, plan)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp classify_archive_or_page_validation_path(path, plan) do
|
||||
cond do
|
||||
Regex.match?(~r|^/(\d{4})/(\d{2})/(\d{2})(?:/page/\d+)?$|, path) ->
|
||||
[_, year, month, day] = Regex.run(~r|^/(\d{4})/(\d{2})/(\d{2})(?:/page/\d+)?$|, path)
|
||||
update_in(plan.requested_year_month_days, &MapSet.put(&1, "#{year}/#{month}/#{day}"))
|
||||
|
||||
Regex.match?(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) ->
|
||||
[_, year, month] = Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path)
|
||||
update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}"))
|
||||
|
||||
Regex.match?(~r|^/(\d{4})(?:/page/\d+)?$|, path) ->
|
||||
[_, year] = Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path)
|
||||
update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year)))
|
||||
|
||||
path == "/" or Regex.match?(~r|^/page/\d+$|, path) ->
|
||||
%{plan | request_root_routes: true}
|
||||
|
||||
Regex.match?(~r|^/([^/]+)$|, path) ->
|
||||
[_, slug] = Regex.run(~r|^/([^/]+)$|, path)
|
||||
update_in(plan.requested_page_slugs, &MapSet.put(&1, slug))
|
||||
|
||||
true ->
|
||||
%{plan | requires_fallback_section_render: true}
|
||||
end
|
||||
end
|
||||
|
||||
@spec build_targeted_validation_plan(map(), [map()]) :: map()
|
||||
def build_targeted_validation_plan(initial_plan, published_posts) do
|
||||
if initial_plan.requires_fallback_section_render do
|
||||
@@ -334,10 +346,14 @@ defmodule BDS.Generation.Validation do
|
||||
[:requested_year_months],
|
||||
&MapSet.put(&1, route_month_key(route.year, route.month))
|
||||
)
|
||||
|> update_in(
|
||||
[:requested_year_month_days],
|
||||
&MapSet.put(&1, route_day_key(route.year, route.month, route.day))
|
||||
)
|
||||
|> Map.put(:request_root_routes, true)
|
||||
|
||||
post ->
|
||||
{year, month, _day} = local_date_parts!(post.created_at)
|
||||
{year, month, day} = local_date_parts!(post.created_at)
|
||||
|
||||
acc
|
||||
|> update_in([:requested_category_slugs], fn set ->
|
||||
@@ -355,10 +371,15 @@ defmodule BDS.Generation.Validation do
|
||||
[:requested_year_months],
|
||||
&MapSet.put(&1, route_month_key(year, month))
|
||||
)
|
||||
|> update_in(
|
||||
[:requested_year_month_days],
|
||||
&MapSet.put(&1, route_day_key(year, month, day))
|
||||
)
|
||||
|> Map.put(:request_root_routes, true)
|
||||
end
|
||||
end
|
||||
)
|
||||
|> cascade_requested_date_archives()
|
||||
|
||||
language_plans =
|
||||
initial_plan.language_plans
|
||||
@@ -384,7 +405,8 @@ defmodule BDS.Generation.Validation do
|
||||
post.slug == route.slug and year == route.year and month == route.month and day == route.day
|
||||
end
|
||||
|
||||
defp route_key(year, month, day, slug) do
|
||||
@spec route_key(integer(), integer(), integer(), String.t()) :: String.t()
|
||||
def route_key(year, month, day, slug) do
|
||||
"#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}/#{String.pad_leading(Integer.to_string(day), 2, "0")}/#{slug}"
|
||||
end
|
||||
|
||||
@@ -392,6 +414,33 @@ defmodule BDS.Generation.Validation do
|
||||
"#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}"
|
||||
end
|
||||
|
||||
defp route_day_key(year, month, day) do
|
||||
"#{route_month_key(year, month)}/#{String.pad_leading(Integer.to_string(day), 2, "0")}"
|
||||
end
|
||||
|
||||
defp cascade_requested_date_archives(plan) do
|
||||
plan
|
||||
|> cascade_requested_year_month_days()
|
||||
|> cascade_requested_year_months()
|
||||
end
|
||||
|
||||
defp cascade_requested_year_month_days(plan) do
|
||||
Enum.reduce(plan.requested_year_month_days, plan, fn year_month_day, acc ->
|
||||
[year, month, _day] = String.split(year_month_day, "/", parts: 3)
|
||||
|
||||
acc
|
||||
|> update_in([:requested_year_months], &MapSet.put(&1, "#{year}/#{month}"))
|
||||
|> update_in([:requested_years], &MapSet.put(&1, String.to_integer(year)))
|
||||
end)
|
||||
end
|
||||
|
||||
defp cascade_requested_year_months(plan) do
|
||||
Enum.reduce(plan.requested_year_months, plan, fn year_month, acc ->
|
||||
[year | _rest] = String.split(year_month, "/", parts: 2)
|
||||
update_in(acc.requested_years, &MapSet.put(&1, String.to_integer(year)))
|
||||
end)
|
||||
end
|
||||
|
||||
defp extract_language_path(path, additional_languages) do
|
||||
case Regex.run(~r|^/([a-z]{2,3})(/.*)?$|, path) do
|
||||
[_, language, suffix] ->
|
||||
@@ -413,83 +462,6 @@ defmodule BDS.Generation.Validation do
|
||||
end
|
||||
end
|
||||
|
||||
@spec targeted_output?(String.t(), map(), String.t() | nil, [String.t()]) :: boolean()
|
||||
def targeted_output?(relative_path, targeted_plan, main_language, additional_languages) do
|
||||
{language, stripped_path} =
|
||||
extract_relative_output_language(relative_path, additional_languages)
|
||||
|
||||
plan =
|
||||
case language do
|
||||
nil -> targeted_plan
|
||||
value -> Map.get(targeted_plan.language_plans, value, empty_validation_path_plan())
|
||||
end
|
||||
|
||||
targeted_output_for_plan?(stripped_path, plan, main_language == language or is_nil(language))
|
||||
end
|
||||
|
||||
defp extract_relative_output_language(relative_path, additional_languages) do
|
||||
segments = String.split(relative_path, "/", trim: true)
|
||||
|
||||
case segments do
|
||||
[language | rest] ->
|
||||
if language in additional_languages do
|
||||
{language, Path.join(rest)}
|
||||
else
|
||||
{nil, relative_path}
|
||||
end
|
||||
|
||||
_other ->
|
||||
{nil, relative_path}
|
||||
end
|
||||
end
|
||||
|
||||
defp targeted_output_for_plan?(
|
||||
_relative_path,
|
||||
%{requires_fallback_section_render: true},
|
||||
_main?
|
||||
),
|
||||
do: true
|
||||
|
||||
defp targeted_output_for_plan?(relative_path, plan, _main?) do
|
||||
cond do
|
||||
relative_path in ["index.html", "404.html", "feed.xml", "atom.xml"] ->
|
||||
plan.request_root_routes
|
||||
|
||||
Regex.match?(~r|^category/([^/]+)(?:/page/\d+)?/index\.html$|, relative_path) ->
|
||||
[_, slug] = Regex.run(~r|^category/([^/]+)(?:/page/\d+)?/index\.html$|, relative_path)
|
||||
MapSet.member?(plan.requested_category_slugs, slug)
|
||||
|
||||
Regex.match?(~r|^tag/([^/]+)/index\.html$|, relative_path) ->
|
||||
[_, slug] = Regex.run(~r|^tag/([^/]+)/index\.html$|, relative_path)
|
||||
MapSet.member?(plan.requested_tag_slugs, slug)
|
||||
|
||||
Regex.match?(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path) ->
|
||||
[_, year, month, day, slug] =
|
||||
Regex.run(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path)
|
||||
|
||||
MapSet.member?(
|
||||
plan.requested_post_routes,
|
||||
route_key(
|
||||
String.to_integer(year),
|
||||
String.to_integer(month),
|
||||
String.to_integer(day),
|
||||
slug
|
||||
)
|
||||
)
|
||||
|
||||
Regex.match?(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path) ->
|
||||
[_, year, month] = Regex.run(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path)
|
||||
MapSet.member?(plan.requested_year_months, "#{year}/#{month}")
|
||||
|
||||
Regex.match?(~r|^(\d{4})/index\.html$|, relative_path) ->
|
||||
[_, year] = Regex.run(~r|^(\d{4})/index\.html$|, relative_path)
|
||||
MapSet.member?(plan.requested_years, String.to_integer(year))
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@spec route_html_path?(String.t()) :: boolean()
|
||||
def route_html_path?(relative_path), do: String.ends_with?(relative_path, "index.html")
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule BDS.Rendering.PostRendering do
|
||||
metadata = RenderMetadata.project_metadata(project_id)
|
||||
template_context = TemplateSelection.template_render_context(project_id)
|
||||
|
||||
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
|
||||
language = MapUtils.attr(assigns, :language) || metadata.main_language || "en"
|
||||
|
||||
main_language = metadata.main_language || language
|
||||
post_record = Map.get(assigns, :_post_record) || load_post_record(assigns)
|
||||
@@ -58,24 +58,26 @@ defmodule BDS.Rendering.PostRendering do
|
||||
|
||||
%{
|
||||
language: language,
|
||||
language_prefix: assign_override(
|
||||
assigns,
|
||||
:language_prefix,
|
||||
"language_prefix",
|
||||
LinksAndLanguages.language_prefix(language, main_language)
|
||||
),
|
||||
language_prefix:
|
||||
assign_override(
|
||||
assigns,
|
||||
:language_prefix,
|
||||
"language_prefix",
|
||||
LinksAndLanguages.language_prefix(language, main_language)
|
||||
),
|
||||
page_title:
|
||||
Map.get(
|
||||
assigns,
|
||||
:page_title,
|
||||
MapUtils.attr(assigns, :title)
|
||||
),
|
||||
pico_stylesheet_href: assign_override(
|
||||
assigns,
|
||||
:pico_stylesheet_href,
|
||||
"pico_stylesheet_href",
|
||||
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
|
||||
),
|
||||
pico_stylesheet_href:
|
||||
assign_override(
|
||||
assigns,
|
||||
:pico_stylesheet_href,
|
||||
"pico_stylesheet_href",
|
||||
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
|
||||
),
|
||||
html_theme_attribute:
|
||||
assign_override(assigns, :html_theme_attribute, "html_theme_attribute", nil),
|
||||
blog_languages: RenderMetadata.blog_languages(metadata, language),
|
||||
|
||||
Reference in New Issue
Block a user