fix: incremental rendering more on par with bDS

This commit is contained in:
2026-07-02 18:31:17 +02:00
parent 43f04dcd86
commit 1487c0337e
14 changed files with 2003 additions and 504 deletions

View File

@@ -263,27 +263,39 @@ defmodule BDS.Desktop.ShellCommands do
end end
defp dispatch("apply_site_validation", project, params) do 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( {:ok, prepare_task} =
project, Tasks.submit_task(
"apply_site_validation", "Prepare Validation Apply",
"Apply Site Validation", fn report ->
"Generation", {:ok, preparation} =
fn report_fn -> Generation.prepare_validation_apply(project.id, validation_report,
{:ok, _apply} = on_progress: report
Generation.apply_validation(project.id, report,
on_progress: scaled_progress_reporter(report_fn, 0.0, 0.85)
) )
{:ok, validation} = Map.put(preparation, :project_id, project.id)
Generation.validate_site(project.id, @site_sections, end,
on_progress: scaled_progress_reporter(report_fn, 0.85, 1.0) attrs
) )
site_validation_result(project.id, validation) Task.start(fn ->
end 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 end
defp dispatch("metadata_diff", project, _params) do defp dispatch("metadata_diff", project, _params) do
@@ -463,6 +475,124 @@ defmodule BDS.Desktop.ShellCommands do
end end
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 defp translation_fill_enabled?(metadata) do
([metadata.main_language] ++ metadata.blog_languages) ([metadata.main_language] ++ metadata.blog_languages)
|> Enum.map(fn language -> |> Enum.map(fn language ->

View File

@@ -21,6 +21,7 @@ defmodule BDS.Generation do
import BDS.Generation.Progress import BDS.Generation.Progress
import BDS.Generation.Outputs import BDS.Generation.Outputs
import BDS.Generation.Data import BDS.Generation.Data
import BDS.Generation.Renderers, only: [build_list_posts: 3]
import BDS.Generation.Validation import BDS.Generation.Validation
alias BDS.Generation.GeneratedFileHash alias BDS.Generation.GeneratedFileHash
@@ -227,62 +228,127 @@ defmodule BDS.Generation do
apply_validation(project_id, report, []) apply_validation(project_id, report, [])
end 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()} @spec apply_validation(String.t(), map(), generation_opts()) :: {:ok, map()} | {:error, term()}
def apply_validation(project_id, report, opts) def apply_validation(project_id, report, opts)
when is_binary(project_id) and is_map(report) and is_list(opts) do when is_binary(project_id) and is_map(report) and is_list(opts) do
on_progress = callback(opts) on_progress = callback(opts)
with {:ok, plan} <- plan_generation(project_id, @core_sections) do with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do
expected_outputs = build_outputs(plan) plan = apply_plan.plan
expected_output_map = Map.new(expected_outputs)
project = Projects.get_project!(project_id) project = Projects.get_project!(project_id)
published_posts = list_published_posts(project_id)
targeted_plan = outputs_to_render = targeted_apply_outputs(plan, apply_plan.targeted_plan, plan.sections)
build_targeted_validation_plan(
plan_validation_paths(report_paths(report), additional_languages(plan)),
published_posts
)
outputs_to_render = rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress)
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)
{deleted_url_count, removed_empty_dir_count} = {deleted_url_count, removed_empty_dir_count} =
delete_extra_validation_paths(project_id, project, Map.get(report, :extra_url_paths, [])) delete_extra_validation_paths(project_id, project, Map.get(report, :extra_url_paths, []))
if outputs_to_render != [] or deleted_url_count > 0 do 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 end
{:ok, {:ok,
%{ %{
rendered_url_count: rendered_url_count: rendered_url_count,
Enum.count(outputs_to_render, fn {relative_path, _content} ->
route_html_path?(relative_path)
end),
deleted_url_count: deleted_url_count, deleted_url_count: deleted_url_count,
removed_empty_dir_count: removed_empty_dir_count removed_empty_dir_count: removed_empty_dir_count
}} }}
@@ -372,7 +438,7 @@ defmodule BDS.Generation do
:ok :ok
end end
defp build_outputs(plan) do defp build_outputs(plan, on_output \\ nil) do
data = generation_data(plan) data = generation_data(plan)
published_translations = flattened_generation_translations(data.translations_by_post) published_translations = flattened_generation_translations(data.translations_by_post)
translations_by_post_language = translation_lookup_map(published_translations) translations_by_post_language = translation_lookup_map(published_translations)
@@ -419,7 +485,8 @@ defmodule BDS.Generation do
build_core_outputs( build_core_outputs(
plan, plan,
data.published_list_posts, data.published_list_posts,
localized_list_posts_by_language localized_list_posts_by_language,
on_output
) )
else else
[] []
@@ -432,7 +499,8 @@ defmodule BDS.Generation do
plan.language, plan.language,
data.published_posts, data.published_posts,
translations_by_post_language, translations_by_post_language,
localized_posts_by_language localized_posts_by_language,
on_output
) )
else else
[] []
@@ -445,14 +513,15 @@ defmodule BDS.Generation do
plan.language, plan.language,
data.published_posts, data.published_posts,
translations_by_post_language, translations_by_post_language,
localized_posts_by_language localized_posts_by_language,
on_output
) )
else else
[] []
end end
archive_outputs = 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 = urls =
(core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs) (core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs)
@@ -606,6 +675,346 @@ defmodule BDS.Generation do
end end
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 defp path_section(relative_path) do
segments = String.split(relative_path, "/", trim: true) segments = String.split(relative_path, "/", trim: true)
@@ -698,11 +1107,34 @@ defmodule BDS.Generation do
:ok :ok
end 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 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) relative_path = url_path_to_relative_index_path(url_path)
full_path = output_path(project, relative_path) full_path = output_path(project, relative_path)
result =
case File.rm(full_path) do case File.rm(full_path) do
:ok -> :ok ->
Repo.delete_all( Repo.delete_all(
@@ -723,25 +1155,22 @@ defmodule BDS.Generation do
{:error, _reason} -> {:error, _reason} ->
{deleted_count, removed_dir_count} {deleted_count, removed_dir_count}
end end
:ok =
report_validation_progress(
on_progress,
0.2 + 0.3 * index / max(total, 1),
"Deleted #{index}/#{total} extra URLs"
)
result
end) end)
end end
defp write_ancillary_validation_outputs(project_id, expected_output_map) do defp ancillary_validation_paths(expected_output_map, :calendar) do
ancillary_paths = expected_output_map
Enum.filter(Map.keys(expected_output_map), fn relative_path -> |> Map.keys()
relative_path == "calendar.json" or String.contains?(relative_path, "pagefind/") |> Enum.filter(&(&1 == "calendar.json"))
end)
Enum.each(ancillary_paths, fn relative_path ->
_ =
write_generated_file(
project_id,
relative_path,
Map.fetch!(expected_output_map, relative_path)
)
end)
:ok
end end
defp output_path(project, relative_path) do defp output_path(project, relative_path) do

View File

@@ -7,6 +7,8 @@ defmodule BDS.Generation.Outputs do
alias BDS.Rendering.TemplateSelection alias BDS.Rendering.TemplateSelection
@type output_callback :: ({String.t(), iodata()} -> any()) | nil
@spec additional_languages(map()) :: [String.t()] @spec additional_languages(map()) :: [String.t()]
def additional_languages(plan) do def additional_languages(plan) do
Enum.reject(plan.blog_languages, &(&1 == plan.language)) Enum.reject(plan.blog_languages, &(&1 == plan.language))
@@ -149,16 +151,17 @@ defmodule BDS.Generation.Outputs do
end) end)
end end
@spec build_archive_outputs(map(), map(), map()) :: [{String.t(), iodata()}] @spec build_archive_outputs(map(), map(), map(), output_callback()) :: [{String.t(), iodata()}]
def build_archive_outputs(plan, post_index, localized_post_indexes) do def build_archive_outputs(plan, post_index, localized_post_indexes, on_output \\ nil) do
category_outputs = category_outputs =
if :category in plan.sections do 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 -> Enum.flat_map(additional_languages(plan), fn language ->
build_category_outputs( build_category_outputs(
plan, plan,
Map.get(localized_post_indexes, language, %{posts_by_category: %{}}).posts_by_category, Map.get(localized_post_indexes, language, %{posts_by_category: %{}}).posts_by_category,
[language] [language],
on_output
) )
end) end)
else else
@@ -167,12 +170,13 @@ defmodule BDS.Generation.Outputs do
tag_outputs = tag_outputs =
if :tag in plan.sections do 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 -> Enum.flat_map(additional_languages(plan), fn language ->
build_tag_outputs( build_tag_outputs(
plan, plan,
Map.get(localized_post_indexes, language, %{posts_by_tag: %{}}).posts_by_tag, Map.get(localized_post_indexes, language, %{posts_by_tag: %{}}).posts_by_tag,
[language] [language],
on_output
) )
end) end)
else else
@@ -181,7 +185,7 @@ defmodule BDS.Generation.Outputs do
date_outputs = date_outputs =
if :date in plan.sections do 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 -> Enum.flat_map(additional_languages(plan), fn language ->
build_date_outputs( build_date_outputs(
plan, plan,
@@ -190,7 +194,8 @@ defmodule BDS.Generation.Outputs do
language, language,
%{posts_by_year: %{}, posts_by_year_month: %{}, posts_by_year_month_day: %{}} %{posts_by_year: %{}, posts_by_year_month: %{}, posts_by_year_month_day: %{}}
), ),
[language] [language],
on_output
) )
end) end)
else else
@@ -200,8 +205,10 @@ defmodule BDS.Generation.Outputs do
category_outputs ++ tag_outputs ++ date_outputs category_outputs ++ tag_outputs ++ date_outputs
end end
@spec build_category_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}] @spec build_category_outputs(map(), map(), [String.t()], output_callback()) :: [
def build_category_outputs(plan, posts_by_category, languages) do {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} -> Enum.flat_map(posts_by_category, fn {category, posts} ->
paginated_posts = Enum.chunk_every(posts, max(plan.max_posts_per_page, 1)) paginated_posts = Enum.chunk_every(posts, max(plan.max_posts_per_page, 1))
category_slug = archive_route_segment(category) category_slug = archive_route_segment(category)
@@ -240,6 +247,7 @@ defmodule BDS.Generation.Outputs do
) )
} }
output =
{ {
archive_path( archive_path(
route_language(plan.language, language), route_language(plan.language, language),
@@ -248,26 +256,37 @@ defmodule BDS.Generation.Outputs do
), ),
render_archive_page(plan, category, page_posts, language, "category", pagination) render_archive_page(plan, category, page_posts, language, "category", pagination)
} }
report_output(output, on_output)
end) end)
end) end)
end) end)
end end
@spec build_tag_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}] @spec build_tag_outputs(map(), map(), [String.t()], output_callback()) :: [
def build_tag_outputs(plan, posts_by_tag, languages) do {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} -> Enum.flat_map(posts_by_tag, fn {tag, posts} ->
tag_slug = archive_route_segment(tag) tag_slug = archive_route_segment(tag)
build_paginated_archive_outputs(plan, languages, ["tag", tag_slug], posts, fn page_posts, build_paginated_archive_outputs(
language, plan,
pagination -> languages,
["tag", tag_slug],
posts,
fn page_posts, language, pagination ->
render_archive_page(plan, tag, page_posts, language, "tag", pagination) render_archive_page(plan, tag, page_posts, language, "tag", pagination)
end) end,
on_output
)
end) end)
end end
@spec build_date_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}] @spec build_date_outputs(map(), map(), [String.t()], output_callback()) :: [
def build_date_outputs(plan, post_index, languages) do {String.t(), iodata()}
]
def build_date_outputs(plan, post_index, languages, on_output \\ nil) do
year_outputs = year_outputs =
Enum.flat_map(post_index.posts_by_year, fn {year, posts} -> Enum.flat_map(post_index.posts_by_year, fn {year, posts} ->
build_paginated_archive_outputs( build_paginated_archive_outputs(
@@ -284,7 +303,8 @@ defmodule BDS.Generation.Outputs do
language, language,
pagination pagination
) )
end end,
on_output
) )
end) end)
@@ -292,9 +312,12 @@ defmodule BDS.Generation.Outputs do
Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} -> Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
[year, month] = String.split(year_month, "/", parts: 2) [year, month] = String.split(year_month, "/", parts: 2)
build_paginated_archive_outputs(plan, languages, [year, month], posts, fn page_posts, build_paginated_archive_outputs(
language, plan,
pagination -> languages,
[year, month],
posts,
fn page_posts, language, pagination ->
render_date_archive_page( render_date_archive_page(
plan, plan,
"#{year}-#{month}", "#{year}-#{month}",
@@ -303,16 +326,21 @@ defmodule BDS.Generation.Outputs do
language, language,
pagination pagination
) )
end) end,
on_output
)
end) end)
day_outputs = day_outputs =
Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} -> 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) [year, month, day] = String.split(year_month_day, "/", parts: 3)
build_paginated_archive_outputs(plan, languages, [year, month, day], posts, fn page_posts, build_paginated_archive_outputs(
language, plan,
pagination -> languages,
[year, month, day],
posts,
fn page_posts, language, pagination ->
render_date_archive_page( render_date_archive_page(
plan, plan,
"#{year}-#{month}-#{day}", "#{year}-#{month}-#{day}",
@@ -326,25 +354,31 @@ defmodule BDS.Generation.Outputs do
language, language,
pagination pagination
) )
end) end,
on_output
)
end) end)
year_outputs ++ month_outputs ++ day_outputs year_outputs ++ month_outputs ++ day_outputs
end end
@spec build_core_outputs(map(), [map()], map()) :: [{String.t(), iodata()}] @spec build_core_outputs(map(), [map()], map(), output_callback()) :: [{String.t(), iodata()}]
def build_core_outputs(plan, published_posts, localized_posts_by_language) do def build_core_outputs(plan, published_posts, localized_posts_by_language, on_output \\ nil) do
language = plan.language language = plan.language
additional_languages = Enum.reject(plan.blog_languages, &(&1 == language)) additional_languages = Enum.reject(plan.blog_languages, &(&1 == language))
main_posts = build_list_posts(plan.base_url, published_posts, nil) 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)}, {"404.html", render_not_found_output(plan, language)},
{"feed.xml", render_feed(plan, language, published_posts)}, {"feed.xml", render_feed(plan, language, published_posts)},
{"atom.xml", render_atom(plan, language, published_posts)}, {"atom.xml", render_atom(plan, language, published_posts)},
{"calendar.json", render_calendar(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 -> Enum.flat_map(additional_languages, fn localized_language ->
localized_prefix = route_language(plan.language, localized_language) localized_prefix = route_language(plan.language, localized_language)
localized_source_posts = Map.get(localized_posts_by_language, localized_language, []) localized_source_posts = Map.get(localized_posts_by_language, localized_language, [])
@@ -352,7 +386,7 @@ defmodule BDS.Generation.Outputs do
localized_posts = localized_posts =
build_list_posts(plan.base_url, localized_source_posts, localized_prefix) build_list_posts(plan.base_url, localized_source_posts, localized_prefix)
build_root_outputs(plan, localized_language, localized_posts) ++ (build_root_outputs(plan, localized_language, localized_posts, on_output) ++
[ [
{Path.join(localized_language, "404.html"), {Path.join(localized_language, "404.html"),
render_not_found_output(plan, localized_language)}, render_not_found_output(plan, localized_language)},
@@ -360,11 +394,12 @@ defmodule BDS.Generation.Outputs do
render_feed(plan, localized_language, localized_source_posts)}, render_feed(plan, localized_language, localized_source_posts)},
{Path.join(localized_language, "atom.xml"), {Path.join(localized_language, "atom.xml"),
render_atom(plan, localized_language, localized_source_posts)} render_atom(plan, localized_language, localized_source_posts)}
] ])
|> Enum.map(&report_output(&1, on_output))
end) end)
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()} {String.t(), iodata()}
] ]
def build_page_outputs( def build_page_outputs(
@@ -372,17 +407,20 @@ defmodule BDS.Generation.Outputs do
main_language, main_language,
published_posts, published_posts,
translations_by_post_language, translations_by_post_language,
localized_posts_by_language localized_posts_by_language,
on_output \\ nil
) do ) do
page_outputs = page_outputs =
published_posts published_posts
|> Enum.filter(&("page" in (&1.categories || []))) |> Enum.filter(&("page" in (&1.categories || [])))
|> Enum.map(fn post -> |> Enum.map(fn post ->
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, 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) body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
effective_slug = effective_template_slug(project_id, post) effective_slug = effective_template_slug(project_id, post)
output =
{page_output_path(post.slug, nil), {page_output_path(post.slug, nil),
render_post_output( render_post_output(
project_id, project_id,
@@ -392,7 +430,7 @@ defmodule BDS.Generation.Outputs do
title: canonical_variant.title, title: canonical_variant.title,
content: body, content: body,
slug: post.slug, slug: post.slug,
language: canonical_variant.language, language: render_language,
excerpt: canonical_variant.excerpt, excerpt: canonical_variant.excerpt,
_post_record: canonical_variant _post_record: canonical_variant
}, },
@@ -401,10 +439,12 @@ defmodule BDS.Generation.Outputs do
canonical_variant.title, canonical_variant.title,
body, body,
post.slug, post.slug,
canonical_variant.language render_language
) )
end end
)} )}
report_output(output, on_output)
end) end)
translation_page_outputs = translation_page_outputs =
@@ -413,10 +453,12 @@ defmodule BDS.Generation.Outputs do
posts posts
|> Enum.filter(&("page" in (&1.categories || []))) |> Enum.filter(&("page" in (&1.categories || [])))
|> Enum.map(fn post -> |> Enum.map(fn post ->
render_language = effective_render_language(post.language, language)
body = load_body(project_id, post.file_path, post.content) body = load_body(project_id, post.file_path, post.content)
effective_slug = effective_template_slug(project_id, post) effective_slug = effective_template_slug(project_id, post)
output =
{page_output_path(post.slug, language), {page_output_path(post.slug, language),
render_post_output( render_post_output(
project_id, project_id,
@@ -426,20 +468,24 @@ defmodule BDS.Generation.Outputs do
title: post.title, title: post.title,
content: body, content: body,
slug: post.slug, slug: post.slug,
language: post.language, language: render_language,
excerpt: post.excerpt, excerpt: post.excerpt,
_post_record: post _post_record: post
}, },
fn -> render_post_page(post.title, body, post.slug, post.language) end fn -> render_post_page(post.title, body, post.slug, render_language) end
)} )}
report_output(output, on_output)
end) end)
end) end)
page_outputs ++ translation_page_outputs page_outputs ++ translation_page_outputs
end end
@spec build_root_outputs(map(), String.t(), [map()]) :: [{String.t(), iodata()}] @spec build_root_outputs(map(), String.t(), [map()], output_callback()) :: [
def build_root_outputs(plan, language, posts) do {String.t(), iodata()}
]
def build_root_outputs(plan, language, posts, on_output \\ nil) do
total_items = length(posts) total_items = length(posts)
total_pages = page_count(total_items, plan.max_posts_per_page) total_pages = page_count(total_items, plan.max_posts_per_page)
@@ -449,6 +495,7 @@ defmodule BDS.Generation.Outputs do
|> Enum.map(fn {page_posts, page_number} -> |> Enum.map(fn {page_posts, page_number} ->
route_language = route_language(plan.language, language) route_language = route_language(plan.language, language)
output =
{root_output_path(route_language, page_number), {root_output_path(route_language, page_number),
render_list_output( render_list_output(
plan, plan,
@@ -466,13 +513,28 @@ defmodule BDS.Generation.Outputs do
), ),
fn -> render_home(plan, language) end fn -> render_home(plan, language) end
)} )}
report_output(output, on_output)
end) end)
end end
@spec build_paginated_archive_outputs(map(), [String.t()], [String.t()], [map()], (... -> @spec build_paginated_archive_outputs(
iodata())) :: map(),
[String.t()],
[String.t()],
[map()],
(... -> iodata()),
output_callback()
) ::
[{String.t(), iodata()}] [{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_items = length(posts)
total_pages = page_count(total_items, plan.max_posts_per_page) total_pages = page_count(total_items, plan.max_posts_per_page)
@@ -483,6 +545,7 @@ defmodule BDS.Generation.Outputs do
Enum.map(languages, fn language -> Enum.map(languages, fn language ->
route_language = route_language(plan.language, language) route_language = route_language(plan.language, language)
output =
{archive_path(route_language, segments, page_number), {archive_path(route_language, segments, page_number),
render_fun.( render_fun.(
page_posts, page_posts,
@@ -496,11 +559,14 @@ defmodule BDS.Generation.Outputs do
segments segments
) )
)} )}
report_output(output, on_output)
end) end)
end) end)
end end
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map()) :: [ @spec build_single_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) ::
[
{String.t(), iodata()} {String.t(), iodata()}
] ]
def build_single_outputs( def build_single_outputs(
@@ -508,15 +574,18 @@ defmodule BDS.Generation.Outputs do
main_language, main_language,
published_posts, published_posts,
translations_by_post_language, translations_by_post_language,
localized_posts_by_language localized_posts_by_language,
on_output \\ nil
) do ) do
post_outputs = post_outputs =
Enum.map(published_posts, fn post -> Enum.map(published_posts, fn post ->
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, 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) body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
effective_slug = effective_template_slug(project_id, post) effective_slug = effective_template_slug(project_id, post)
output =
{post_output_path(post), {post_output_path(post),
render_post_output( render_post_output(
project_id, project_id,
@@ -526,7 +595,7 @@ defmodule BDS.Generation.Outputs do
title: canonical_variant.title, title: canonical_variant.title,
content: body, content: body,
slug: post.slug, slug: post.slug,
language: canonical_variant.language, language: render_language,
excerpt: canonical_variant.excerpt, excerpt: canonical_variant.excerpt,
_post_record: canonical_variant _post_record: canonical_variant
}, },
@@ -535,20 +604,24 @@ defmodule BDS.Generation.Outputs do
canonical_variant.title, canonical_variant.title,
body, body,
post.slug, post.slug,
canonical_variant.language render_language
) )
end end
)} )}
report_output(output, on_output)
end) end)
translation_outputs = translation_outputs =
localized_posts_by_language localized_posts_by_language
|> Enum.flat_map(fn {language, posts} -> |> Enum.flat_map(fn {language, posts} ->
Enum.map(posts, fn post -> Enum.map(posts, fn post ->
render_language = effective_render_language(post.language, language)
body = load_body(project_id, post.file_path, post.content) body = load_body(project_id, post.file_path, post.content)
effective_slug = effective_template_slug(project_id, post) effective_slug = effective_template_slug(project_id, post)
output =
{post_output_path(post, language), {post_output_path(post, language),
render_post_output( render_post_output(
project_id, project_id,
@@ -558,12 +631,14 @@ defmodule BDS.Generation.Outputs do
title: post.title, title: post.title,
content: body, content: body,
slug: post.slug, slug: post.slug,
language: post.language, language: render_language,
excerpt: post.excerpt, excerpt: post.excerpt,
_post_record: post _post_record: post
}, },
fn -> render_post_page(post.title, body, post.slug, post.language) end fn -> render_post_page(post.title, body, post.slug, render_language) end
)} )}
report_output(output, on_output)
end) end)
end) end)
@@ -583,4 +658,23 @@ defmodule BDS.Generation.Outputs do
) )
end end
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 end

View File

@@ -245,7 +245,9 @@ defmodule BDS.Generation.Validation do
requested_tag_slugs: MapSet.new(), requested_tag_slugs: MapSet.new(),
requested_years: MapSet.new(), requested_years: MapSet.new(),
requested_year_months: MapSet.new(), requested_year_months: MapSet.new(),
requested_year_month_days: MapSet.new(),
requested_post_routes: [], requested_post_routes: [],
requested_page_slugs: MapSet.new(),
language_plans: %{} language_plans: %{}
} }
end end
@@ -277,27 +279,37 @@ defmodule BDS.Generation.Validation do
) )
nil -> nil ->
case Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) do classify_archive_or_page_validation_path(path, plan)
[_, year, month] -> 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}")) update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}"))
nil -> Regex.match?(~r|^/(\d{4})(?:/page/\d+)?$|, path) ->
case Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path) do [_, year] = Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path)
[_, year] ->
update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year))) update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year)))
nil -> path == "/" or Regex.match?(~r|^/page/\d+$|, path) ->
if path == "/" or Regex.match?(~r|^/page/\d+$|, path) do
%{plan | request_root_routes: true} %{plan | request_root_routes: true}
else
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} %{plan | requires_fallback_section_render: true}
end end
end end
end
end
end
end
end
@spec build_targeted_validation_plan(map(), [map()]) :: map() @spec build_targeted_validation_plan(map(), [map()]) :: map()
def build_targeted_validation_plan(initial_plan, published_posts) do def build_targeted_validation_plan(initial_plan, published_posts) do
@@ -334,10 +346,14 @@ defmodule BDS.Generation.Validation do
[:requested_year_months], [:requested_year_months],
&MapSet.put(&1, route_month_key(route.year, route.month)) &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) |> Map.put(:request_root_routes, true)
post -> post ->
{year, month, _day} = local_date_parts!(post.created_at) {year, month, day} = local_date_parts!(post.created_at)
acc acc
|> update_in([:requested_category_slugs], fn set -> |> update_in([:requested_category_slugs], fn set ->
@@ -355,10 +371,15 @@ defmodule BDS.Generation.Validation do
[:requested_year_months], [:requested_year_months],
&MapSet.put(&1, route_month_key(year, month)) &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) |> Map.put(:request_root_routes, true)
end end
end end
) )
|> cascade_requested_date_archives()
language_plans = language_plans =
initial_plan.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 post.slug == route.slug and year == route.year and month == route.month and day == route.day
end 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}" "#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}/#{String.pad_leading(Integer.to_string(day), 2, "0")}/#{slug}"
end end
@@ -392,6 +414,33 @@ defmodule BDS.Generation.Validation do
"#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}" "#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}"
end 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 defp extract_language_path(path, additional_languages) do
case Regex.run(~r|^/([a-z]{2,3})(/.*)?$|, path) do case Regex.run(~r|^/([a-z]{2,3})(/.*)?$|, path) do
[_, language, suffix] -> [_, language, suffix] ->
@@ -413,83 +462,6 @@ defmodule BDS.Generation.Validation do
end end
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() @spec route_html_path?(String.t()) :: boolean()
def route_html_path?(relative_path), do: String.ends_with?(relative_path, "index.html") def route_html_path?(relative_path), do: String.ends_with?(relative_path, "index.html")

View File

@@ -20,7 +20,7 @@ defmodule BDS.Rendering.PostRendering do
metadata = RenderMetadata.project_metadata(project_id) metadata = RenderMetadata.project_metadata(project_id)
template_context = TemplateSelection.template_render_context(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 main_language = metadata.main_language || language
post_record = Map.get(assigns, :_post_record) || load_post_record(assigns) post_record = Map.get(assigns, :_post_record) || load_post_record(assigns)
@@ -58,7 +58,8 @@ defmodule BDS.Rendering.PostRendering do
%{ %{
language: language, language: language,
language_prefix: assign_override( language_prefix:
assign_override(
assigns, assigns,
:language_prefix, :language_prefix,
"language_prefix", "language_prefix",
@@ -70,7 +71,8 @@ defmodule BDS.Rendering.PostRendering do
:page_title, :page_title,
MapUtils.attr(assigns, :title) MapUtils.attr(assigns, :title)
), ),
pico_stylesheet_href: assign_override( pico_stylesheet_href:
assign_override(
assigns, assigns,
:pico_stylesheet_href, :pico_stylesheet_href,
"pico_stylesheet_href", "pico_stylesheet_href",

View File

@@ -221,6 +221,86 @@ msgstr "Auswahl anwenden"
msgid "Apply Theme" msgid "Apply Theme"
msgstr "Theme anwenden" msgstr "Theme anwenden"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Website-Validierung anwenden"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Aufgaben zum Anwenden der Website-Validierung eingereiht"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Suchindex aufbauen"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Suchindex aufgebaut"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Überzählige URLs werden gelöscht..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "Keine überzähligen URLs zu löschen"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Schritte zum Anwenden der Validierung werden geplant..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Anwenden der Validierung vorbereiten"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Kalender neu erzeugt"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Kategoriearchive rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Kategoriearchive gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Datumsarchive rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Datumsarchive gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Einzelbeiträge rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Einzelbeiträge gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Website-Kern rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Website-Kern gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Tag-Archive rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Tag-Archive gerendert"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Anwenden der Validierung vorbereitet"
#: lib/bds/desktop/shell_data.ex:182 #: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331 #: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr ""
msgid "Apply Theme" msgid "Apply Theme"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr ""
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr ""
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr ""
#: lib/bds/desktop/shell_data.ex:182 #: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331 #: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr "Aplicar seleccionados"
msgid "Apply Theme" msgid "Apply Theme"
msgstr "Aplicar tema" msgstr "Aplicar tema"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Aplicar validación del sitio"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Tareas para aplicar la validación del sitio en cola"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Crear índice de búsqueda"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Índice de búsqueda creado"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Eliminando URLs sobrantes..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "No hay URLs sobrantes para eliminar"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Planificando pasos para aplicar la validación..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Preparar aplicación de validación"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Calendario regenerado"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Renderizar archivos de categorías"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Archivos de categorías renderizados"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Renderizar archivos por fecha"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Archivos por fecha renderizados"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Renderizar entradas individuales"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Entradas individuales renderizadas"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Renderizar núcleo del sitio"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Núcleo del sitio renderizado"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Renderizar archivos de etiquetas"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Archivos de etiquetas renderizados"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Aplicación de validación preparada"
#: lib/bds/desktop/shell_data.ex:182 #: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331 #: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr "Appliquer la selection"
msgid "Apply Theme" msgid "Apply Theme"
msgstr "Appliquer le thème" msgstr "Appliquer le thème"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Appliquer la validation du site"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Tâches dapplication de la validation du site mises en file"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Construire lindex de recherche"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Index de recherche construit"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Suppression des URLs en trop..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "Aucune URL en trop à supprimer"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Planification de lapplication de la validation..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Préparer lapplication de la validation"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Calendrier régénéré"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Rendre les archives de catégories"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Archives de catégories rendues"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Rendre les archives par date"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Archives par date rendues"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Rendre les articles individuels"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Articles individuels rendus"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Rendre le cœur du site"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Cœur du site rendu"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Rendre les archives de tags"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Archives de tags rendues"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Application de la validation préparée"
#: lib/bds/desktop/shell_data.ex:182 #: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331 #: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr "Applica selezionati"
msgid "Apply Theme" msgid "Apply Theme"
msgstr "Applica tema" msgstr "Applica tema"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Applica validazione sito"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Attività per applicare la validazione sito in coda"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Crea indice di ricerca"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Indice di ricerca creato"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Eliminazione URL extra..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "Nessun URL extra da eliminare"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Pianificazione dei passaggi di applicazione della validazione..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Prepara applicazione validazione"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Calendario rigenerato"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Renderizza archivi categorie"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Archivi categorie renderizzati"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Renderizza archivi data"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Archivi data renderizzati"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Renderizza post singoli"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Post singoli renderizzati"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Renderizza nucleo sito"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Nucleo sito renderizzato"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Renderizza archivi tag"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Archivi tag renderizzati"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Applicazione della validazione preparata"
#: lib/bds/desktop/shell_data.ex:182 #: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331 #: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format

View File

@@ -11,7 +11,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:333 #: lib/bds/desktop/shell_live/misc_editor.ex:319
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "%{canonical} = %{translation}" msgid "%{canonical} = %{translation}"
msgstr "" msgstr ""
@@ -92,7 +92,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:888 #: lib/bds/desktop/shell_live/post_editor.ex:918
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -234,6 +234,86 @@ msgstr ""
msgid "Apply Theme" msgid "Apply Theme"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr ""
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr ""
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr ""
#: lib/bds/desktop/shell_data.ex:182 #: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331 #: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -272,12 +352,12 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:422 #: lib/bds/desktop/shell_live.ex:422
#: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:160 #: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:353 #: lib/bds/desktop/shell_live/media_editor.ex:364
#: lib/bds/desktop/shell_live/media_editor.ex:546 #: lib/bds/desktop/shell_live/media_editor.ex:557
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:755 #: lib/bds/desktop/shell_live/post_editor.ex:763
#: lib/bds/desktop/shell_live/post_editor.ex:804 #: lib/bds/desktop/shell_live/post_editor.ex:812
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "" msgstr ""
@@ -380,8 +460,8 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor.ex:1024 #: lib/bds/desktop/shell_live/import_editor.ex:1024
#: lib/bds/desktop/shell_live/import_editor.ex:1188 #: lib/bds/desktop/shell_live/import_editor.ex:1188
#: lib/bds/desktop/shell_live/index.html.heex:336 #: lib/bds/desktop/shell_live/index.html.heex:336
#: lib/bds/desktop/shell_live/misc_editor.ex:755 #: lib/bds/desktop/shell_live/misc_editor.ex:741
#: lib/bds/desktop/shell_live/misc_editor.ex:756 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:256 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:256
#: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:59 #: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:59
#: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:75 #: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:75
@@ -439,7 +519,7 @@ msgstr ""
msgid "Check Syntax" msgid "Check Syntax"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:486 #: lib/bds/desktop/shell_live/misc_editor.ex:472
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}" msgid "Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}"
msgstr "" msgstr ""
@@ -675,7 +755,7 @@ msgstr ""
msgid "Delete Media" msgid "Delete Media"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:396 #: lib/bds/desktop/shell_live/media_editor.ex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Translation" msgid "Delete Translation"
msgstr "" msgstr ""
@@ -691,7 +771,7 @@ msgstr ""
msgid "Delete this unpublished draft" msgid "Delete this unpublished draft"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:120 #: lib/bds/desktop/shell_live/misc_editor.ex:106
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk" msgid "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk"
msgstr "" msgstr ""
@@ -716,14 +796,14 @@ msgstr ""
msgid "Detect" msgid "Detect"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:159 #: lib/bds/desktop/shell_live/media_editor.ex:170
#: lib/bds/desktop/shell_live/media_editor.ex:198
#: lib/bds/desktop/shell_live/media_editor.ex:203
#: lib/bds/desktop/shell_live/media_editor.ex:209 #: lib/bds/desktop/shell_live/media_editor.ex:209
#: lib/bds/desktop/shell_live/media_editor.ex:214
#: lib/bds/desktop/shell_live/media_editor.ex:220
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:754 #: lib/bds/desktop/shell_live/post_editor.ex:762
#: lib/bds/desktop/shell_live/post_editor.ex:783 #: lib/bds/desktop/shell_live/post_editor.ex:791
#: lib/bds/desktop/shell_live/post_editor.ex:789 #: lib/bds/desktop/shell_live/post_editor.ex:797
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "" msgstr ""
@@ -854,8 +934,8 @@ msgstr ""
msgid "Editor Settings" msgid "Editor Settings"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:757 #: lib/bds/desktop/shell_live/misc_editor.ex:743
#: lib/bds/desktop/shell_live/misc_editor.ex:780 #: lib/bds/desktop/shell_live/misc_editor.ex:766
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Embeddings" msgid "Embeddings"
msgstr "" msgstr ""
@@ -980,11 +1060,11 @@ msgstr ""
msgid "Find Duplicate Posts" msgid "Find Duplicate Posts"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:176 #: lib/bds/desktop/shell_live/misc_editor.ex:162
#: lib/bds/desktop/shell_live/misc_editor.ex:182 #: lib/bds/desktop/shell_live/misc_editor.ex:168
#: lib/bds/desktop/shell_live/misc_editor.ex:209 #: lib/bds/desktop/shell_live/misc_editor.ex:195
#: lib/bds/desktop/shell_live/misc_editor.ex:216 #: lib/bds/desktop/shell_live/misc_editor.ex:202
#: lib/bds/desktop/shell_live/misc_editor.ex:503 #: lib/bds/desktop/shell_live/misc_editor.ex:489
#: lib/bds/ui/registry.ex:139 #: lib/bds/ui/registry.ex:139
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Find Duplicates" msgid "Find Duplicates"
@@ -1013,7 +1093,7 @@ msgstr ""
msgid "Git" msgid "Git"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:560 #: lib/bds/desktop/shell_live/misc_editor.ex:546
#: lib/bds/ui/registry.ex:113 #: lib/bds/ui/registry.ex:113
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Diff" msgid "Git Diff"
@@ -1048,8 +1128,8 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:711 #: lib/bds/desktop/shell_live/media_editor.ex:731
#: lib/bds/desktop/shell_live/post_editor.ex:1006 #: lib/bds/desktop/shell_live/post_editor.ex:1036
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "" msgstr ""
@@ -1219,8 +1299,8 @@ msgstr ""
msgid "Language" msgid "Language"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:210 #: lib/bds/desktop/shell_live/media_editor.ex:221
#: lib/bds/desktop/shell_live/post_editor.ex:790 #: lib/bds/desktop/shell_live/post_editor.ex:798
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "" msgstr ""
@@ -1230,7 +1310,7 @@ msgstr ""
msgid "Light" msgid "Light"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:256 #: lib/bds/desktop/shell_live/media_editor.ex:267
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:215 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:215
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Link to Post" msgid "Link to Post"
@@ -1304,7 +1384,7 @@ msgstr ""
msgid "Mapped" msgid "Mapped"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:1009 #: lib/bds/desktop/shell_live/post_editor.ex:1039
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:123 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:123
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1316,10 +1396,10 @@ msgid "Max Posts Per Page"
msgstr "" msgstr ""
#: lib/bds/desktop/menu_bar.ex:168 #: lib/bds/desktop/menu_bar.ex:168
#: lib/bds/desktop/shell_live/media_editor.ex:498 #: lib/bds/desktop/shell_live/media_editor.ex:509
#: lib/bds/desktop/shell_live/media_editor.ex:502 #: lib/bds/desktop/shell_live/media_editor.ex:513
#: lib/bds/desktop/shell_live/misc_editor.ex:749 #: lib/bds/desktop/shell_live/misc_editor.ex:735
#: lib/bds/desktop/shell_live/misc_editor.ex:776 #: lib/bds/desktop/shell_live/misc_editor.ex:762
#: lib/bds/desktop/shell_live/sidebar_components.ex:654 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
@@ -1334,7 +1414,7 @@ msgstr ""
msgid "Media (%{count})" msgid "Media (%{count})"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:498 #: lib/bds/desktop/shell_live/media_editor.ex:509
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Media saved" msgid "Media saved"
msgstr "" msgstr ""
@@ -1361,9 +1441,9 @@ msgid "Metadata"
msgstr "" msgstr ""
#: lib/bds/desktop/menu_bar.ex:185 #: lib/bds/desktop/menu_bar.ex:185
#: lib/bds/desktop/shell_live/misc_editor.ex:228 #: lib/bds/desktop/shell_live/misc_editor.ex:214
#: lib/bds/desktop/shell_live/misc_editor.ex:240 #: lib/bds/desktop/shell_live/misc_editor.ex:226
#: lib/bds/desktop/shell_live/misc_editor.ex:462 #: lib/bds/desktop/shell_live/misc_editor.ex:448
#: lib/bds/ui/registry.ex:111 #: lib/bds/ui/registry.ex:111
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Metadata Diff" msgid "Metadata Diff"
@@ -1465,7 +1545,7 @@ msgstr ""
msgid "No git history yet" msgid "No git history yet"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:473 #: lib/bds/desktop/shell_live/misc_editor.ex:459
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:321 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:321
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:333 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:333
@@ -1505,12 +1585,12 @@ msgstr ""
msgid "No media files" msgid "No media files"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:592 #: lib/bds/desktop/shell_live/misc_editor.ex:578
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No metadata diff items selected" msgid "No metadata diff items selected"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:621 #: lib/bds/desktop/shell_live/misc_editor.ex:607
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No orphan files selected" msgid "No orphan files selected"
msgstr "" msgstr ""
@@ -1535,7 +1615,7 @@ msgstr ""
msgid "No posts yet" msgid "No posts yet"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:589 #: lib/bds/desktop/shell_live/misc_editor.ex:575
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No repair action available" msgid "No repair action available"
msgstr "" msgstr ""
@@ -1560,7 +1640,7 @@ msgstr ""
msgid "No translations" msgid "No translations"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:567 #: lib/bds/desktop/shell_live/misc_editor.ex:553
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No unstaged changes" msgid "No unstaged changes"
msgstr "" msgstr ""
@@ -1775,7 +1855,7 @@ msgstr ""
msgid "Pages (%{count})" msgid "Pages (%{count})"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:176 #: lib/bds/desktop/shell_live/misc_editor.ex:162
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Pair dismissed" msgid "Pair dismissed"
msgstr "" msgstr ""
@@ -1801,17 +1881,17 @@ msgstr ""
msgid "Persist the detected language for this media item" msgid "Persist the detected language for this media item"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:733
#: lib/bds/desktop/shell_live/post_editor.ex:529 #: lib/bds/desktop/shell_live/post_editor.ex:537
#: lib/bds/desktop/shell_live/post_editor.ex:533 #: lib/bds/desktop/shell_live/post_editor.ex:541
#: lib/bds/desktop/shell_live/post_editor.ex:572 #: lib/bds/desktop/shell_live/post_editor.ex:580
#: lib/bds/desktop/shell_live/post_editor.ex:576 #: lib/bds/desktop/shell_live/post_editor.ex:584
#: lib/bds/desktop/shell_live/post_editor.ex:614 #: lib/bds/desktop/shell_live/post_editor.ex:622
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/post_editor.ex:658 #: lib/bds/desktop/shell_live/post_editor.ex:666
#: lib/bds/desktop/shell_live/post_editor.ex:661 #: lib/bds/desktop/shell_live/post_editor.ex:669
#: lib/bds/desktop/shell_live/post_editor.ex:741 #: lib/bds/desktop/shell_live/post_editor.ex:749
#: lib/bds/desktop/shell_live/post_editor.ex:744 #: lib/bds/desktop/shell_live/post_editor.ex:752
#: lib/bds/desktop/shell_live/sidebar_components.ex:651 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
@@ -1836,24 +1916,24 @@ msgstr ""
msgid "Post Template" msgid "Post Template"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:315 #: lib/bds/desktop/shell_live/misc_editor.ex:301
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:572 #: lib/bds/desktop/shell_live/post_editor.ex:580
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:529 #: lib/bds/desktop/shell_live/post_editor.ex:537
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "" msgstr ""
#: lib/bds/desktop/menu_bar.ex:167 #: lib/bds/desktop/menu_bar.ex:167
#: lib/bds/desktop/shell_live/misc_editor.ex:678 #: lib/bds/desktop/shell_live/misc_editor.ex:664
#: lib/bds/desktop/shell_live/misc_editor.ex:775 #: lib/bds/desktop/shell_live/misc_editor.ex:761
#: lib/bds/ui/registry.ex:14 #: lib/bds/ui/registry.ex:14
#: lib/bds/ui/sidebar.ex:271 #: lib/bds/ui/sidebar.ex:271
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1870,7 +1950,7 @@ msgstr ""
msgid "Preferences" msgid "Preferences"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:1010 #: lib/bds/desktop/shell_live/post_editor.ex:1040
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:124 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:124
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1892,8 +1972,8 @@ msgid "Preview unavailable"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/index.html.heex:509 #: lib/bds/desktop/shell_live/index.html.heex:509
#: lib/bds/desktop/shell_live/misc_editor.ex:753 #: lib/bds/desktop/shell_live/misc_editor.ex:739
#: lib/bds/desktop/shell_live/misc_editor.ex:779 #: lib/bds/desktop/shell_live/misc_editor.ex:765
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:27 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:27
#: lib/bds/ui/sidebar.ex:762 #: lib/bds/ui/sidebar.ex:762
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1939,19 +2019,19 @@ msgid "Publish Selected"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:1004 #: lib/bds/desktop/shell_live/post_editor.ex:1034
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:472 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:472
#: lib/bds/ui/sidebar.ex:324 #: lib/bds/ui/sidebar.ex:324
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Published" msgid "Published"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:318 #: lib/bds/desktop/shell_live/misc_editor.ex:304
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Published translation has content stuck in DB instead of filesystem" msgid "Published translation has content stuck in DB instead of filesystem"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:754 #: lib/bds/desktop/shell_live/misc_editor.ex:740
#: lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex:40 #: lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex:40
#: lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex:57 #: lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex:57
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:338 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:338
@@ -2038,8 +2118,8 @@ msgstr ""
msgid "Refresh Online Models" msgid "Refresh Online Models"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:368 #: lib/bds/desktop/shell_live/media_editor.ex:379
#: lib/bds/desktop/shell_live/media_editor.ex:377 #: lib/bds/desktop/shell_live/media_editor.ex:388
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Refresh Translation" msgid "Refresh Translation"
msgstr "" msgstr ""
@@ -2095,14 +2175,14 @@ msgstr ""
msgid "Replace" msgid "Replace"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:142 #: lib/bds/desktop/shell_live/media_editor.ex:153
#: lib/bds/desktop/shell_live/media_editor.ex:150 #: lib/bds/desktop/shell_live/media_editor.ex:161
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:86 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:86
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Replace File" msgid "Replace File"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:116 #: lib/bds/desktop/shell_live/media_editor.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Replace Media File" msgid "Replace Media File"
msgstr "" msgstr ""
@@ -2137,7 +2217,7 @@ msgstr ""
msgid "Result" msgid "Result"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:1005 #: lib/bds/desktop/shell_live/post_editor.ex:1035
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "" msgstr ""
@@ -2183,13 +2263,13 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:328 #: lib/bds/desktop/shell_live/media_editor.ex:339
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Translation" msgid "Save Translation"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:710 #: lib/bds/desktop/shell_live/media_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor.ex:1003 #: lib/bds/desktop/shell_live/post_editor.ex:1033
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "" msgstr ""
@@ -2199,7 +2279,7 @@ msgstr ""
msgid "Scanning entries..." msgid "Scanning entries..."
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:751 #: lib/bds/desktop/shell_live/misc_editor.ex:737
#: lib/bds/desktop/shell_live/sidebar_components.ex:657 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
@@ -2227,7 +2307,7 @@ msgstr ""
msgid "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here." msgid "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here."
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:777 #: lib/bds/desktop/shell_live/misc_editor.ex:763
#: lib/bds/desktop/shell_live/script_editor.ex:119 #: lib/bds/desktop/shell_live/script_editor.ex:119
#: lib/bds/desktop/shell_live/script_editor.ex:124 #: lib/bds/desktop/shell_live/script_editor.ex:124
#: lib/bds/desktop/shell_live/script_editor.ex:130 #: lib/bds/desktop/shell_live/script_editor.ex:130
@@ -2332,7 +2412,7 @@ msgstr ""
msgid "Select an existing category or press Enter to create a new archive entry" msgid "Select an existing category or press Enter to create a new archive entry"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:210 #: lib/bds/desktop/shell_live/misc_editor.ex:196
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Selected pairs dismissed" msgid "Selected pairs dismissed"
msgstr "" msgstr ""
@@ -2366,9 +2446,7 @@ msgstr ""
msgid "Site" msgid "Site"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:95 #: lib/bds/desktop/shell_live/misc_editor.ex:412
#: lib/bds/desktop/shell_live/misc_editor.ex:105
#: lib/bds/desktop/shell_live/misc_editor.ex:426
#: lib/bds/ui/registry.ex:125 #: lib/bds/ui/registry.ex:125
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Site Validation" msgid "Site Validation"
@@ -2510,7 +2588,7 @@ msgstr ""
msgid "Technology" msgid "Technology"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:752 #: lib/bds/desktop/shell_live/misc_editor.ex:738
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:299 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:299
#: lib/bds/desktop/shell_live/sidebar_components.ex:660 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
@@ -2534,7 +2612,7 @@ msgstr ""
msgid "Template syntax is valid" msgid "Template syntax is valid"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:778 #: lib/bds/desktop/shell_live/misc_editor.ex:764
#: lib/bds/desktop/shell_live/template_editor.ex:111 #: lib/bds/desktop/shell_live/template_editor.ex:111
#: lib/bds/desktop/shell_live/template_editor.ex:116 #: lib/bds/desktop/shell_live/template_editor.ex:116
#: lib/bds/desktop/shell_live/template_editor.ex:121 #: lib/bds/desktop/shell_live/template_editor.ex:121
@@ -2658,39 +2736,35 @@ msgstr ""
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:352 #: lib/bds/desktop/shell_live/media_editor.ex:363
#: lib/bds/desktop/shell_live/media_editor.ex:545 #: lib/bds/desktop/shell_live/media_editor.ex:556
#: lib/bds/desktop/shell_live/media_editor.ex:566
#: lib/bds/desktop/shell_live/media_editor.ex:571
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:803 #: lib/bds/desktop/shell_live/post_editor.ex:811
#: lib/bds/desktop/shell_live/post_editor.ex:832
#: lib/bds/desktop/shell_live/post_editor.ex:837
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:105
#: lib/bds/desktop/shell_live/misc_editor.ex:119 #: lib/bds/desktop/shell_live/misc_editor.ex:119
#: lib/bds/desktop/shell_live/misc_editor.ex:133 #: lib/bds/desktop/shell_live/misc_editor.ex:468
#: lib/bds/desktop/shell_live/misc_editor.ex:482
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translation Validation" msgid "Translation Validation"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:312 #: lib/bds/desktop/shell_live/misc_editor.ex:298
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translation language matches canonical post language" msgid "Translation language matches canonical post language"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:321 #: lib/bds/desktop/shell_live/misc_editor.ex:307
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translation points to a missing source post" msgid "Translation points to a missing source post"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:183 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:183
#: lib/bds/desktop/shell_live/misc_editor.ex:748 #: lib/bds/desktop/shell_live/misc_editor.ex:734
#: lib/bds/desktop/shell_live/misc_editor.ex:750 #: lib/bds/desktop/shell_live/misc_editor.ex:736
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:129 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:129
#: lib/bds/ui/registry.ex:131 #: lib/bds/ui/registry.ex:131
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2735,14 +2809,14 @@ msgstr ""
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:269 #: lib/bds/desktop/shell_live/media_editor.ex:280
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unlink from Post" msgid "Unlink from Post"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:709 #: lib/bds/desktop/shell_live/media_editor.ex:729
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:1002 #: lib/bds/desktop/shell_live/post_editor.ex:1032
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2811,11 +2885,6 @@ msgstr ""
msgid "Validate Translations" msgid "Validate Translations"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:96
#, elixir-autogen, elixir-format
msgid "Validation changes applied"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:146 #: lib/bds/desktop/menu_bar.ex:146
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "View" msgid "View"
@@ -3243,12 +3312,12 @@ msgstr ""
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:658 #: lib/bds/desktop/shell_live/post_editor.ex:666
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post archived" msgid "Post archived"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:741 #: lib/bds/desktop/shell_live/post_editor.ex:749
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post unarchived" msgid "Post unarchived"
msgstr "" msgstr ""
@@ -3427,18 +3496,18 @@ msgstr ""
msgid "Open a project before importing a blogmark." msgid "Open a project before importing a blogmark."
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:684 #: lib/bds/desktop/shell_live/post_editor.ex:692
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{name}" msgid "Added %{name}"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:699
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to import %{path}: %{reason}" msgid "Failed to import %{path}: %{reason}"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:683 #: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:690 #: lib/bds/desktop/shell_live/post_editor.ex:698
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Insert Image" msgid "Insert Image"
msgstr "" msgstr ""

View File

@@ -458,10 +458,11 @@ rule SiteValidationScan {
rule SiteValidationApply { rule SiteValidationApply {
when: SiteValidationApplyRequested(report) when: SiteValidationApplyRequested(report)
-- Classifies affected paths into generation sections (core, single, category, tag, date) -- Classifies affected paths into generation sections (core, single, category, tag, date)
-- Renders only affected sections in parallel -- Renders only affected routes (a post's own single page plus only its own
-- categories, tags, and date archives); unaffected routes are left untouched
-- Deletes extra HTML files, removes empty directories -- Deletes extra HTML files, removes empty directories
-- Regenerates calendar if anything changed -- Regenerates calendar if anything changed (feed/atom/404/sitemap/search
-- Rebuilds search index if anything rendered or deleted -- index are NOT rebuilt on apply)
ensures: ApplyValidationRequested(report.project_id, report.affected_sections) ensures: ApplyValidationRequested(report.project_id, report.affected_sections)
} }

View File

@@ -368,6 +368,76 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert Map.has_key?(completed.result.payload.summary, :updated_count) assert Map.has_key?(completed.result.payload.summary, :updated_count)
end end
test "apply_site_validation queues visible apply subtasks and revalidates the site", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Validation Apply Task Post",
content: "Validation apply task body",
language: "en",
categories: ["notes"],
tags: ["validation-task"]
})
assert {:ok, published_post} = Posts.publish_post(post.id)
assert {:ok, _generation} = BDS.Generation.generate_site(project.id, [:core, :single])
post_path = BDS.Generation.post_output_path(published_post)
File.rm!(Path.join([temp_dir, "html", post_path]))
extra_path = Path.join([temp_dir, "html", "obsolete", "index.html"])
File.mkdir_p!(Path.dirname(extra_path))
File.write!(extra_path, "<html>obsolete</html>")
post_url_path = BDS.Generation.Paths.relative_path_to_url_path(post_path)
assert {:ok, report} =
BDS.Generation.validate_site(project.id, [:core, :single, :category, :tag, :date])
assert post_url_path in report.missing_url_paths
assert "/obsolete" in report.extra_url_paths
assert {:ok, result} = ShellCommands.execute("apply_site_validation", %{report: report})
assert result.kind == "task_queued"
assert result.action == "apply_site_validation"
assert result.title == "Apply Site Validation"
assert is_binary(result.task_id)
assert is_binary(result.task_group_id)
tasks =
wait_for_tasks_by_name(
[
"Prepare Validation Apply",
"Render Site Core",
"Render Single Posts",
"Validate Site"
],
&(&1.status == :completed),
5_000
)
assert Enum.all?(tasks, &(&1.group_id == result.task_group_id))
assert Enum.all?(tasks, &(&1.group_name == "Apply Site Validation"))
validation_task = Enum.find(tasks, &(&1.name == "Validate Site"))
assert validation_task.result.kind == "open_editor"
assert validation_task.result.route == "site_validation"
assert validation_task.result.payload.missing_url_paths == []
assert validation_task.result.payload.extra_url_paths == []
assert validation_task.result.payload.updated_post_url_paths == []
end
test "metadata_diff queues a tracked maintenance task and returns the report as an editor payload" do test "metadata_diff queues a tracked maintenance task and returns the report as an editor payload" do
assert {:ok, result} = ShellCommands.execute("metadata_diff") assert {:ok, result} = ShellCommands.execute("metadata_diff")

View File

@@ -914,6 +914,321 @@ defmodule BDS.GenerationTest do
assert clean_report.updated_post_url_paths == [] assert clean_report.updated_post_url_paths == []
end end
test "apply_validation renders only targeted routes and leaves feed/atom/404/pagefind untouched",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post_a} =
Posts.create_post(%{
project_id: project.id,
title: "First Post",
content: "First body",
language: "en"
})
assert {:ok, _published_a} = Posts.publish_post(post_a.id)
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
feed_path = Path.join([temp_dir, "html", "feed.xml"])
atom_path = Path.join([temp_dir, "html", "atom.xml"])
not_found_path = Path.join([temp_dir, "html", "404.html"])
pagefind_path = Path.join([temp_dir, "html", "pagefind", "index.json"])
feed_before = File.read!(feed_path)
atom_before = File.read!(atom_path)
not_found_before = File.read!(not_found_path)
pagefind_before = File.read!(pagefind_path)
# Publishing a new post leaves the generated feed/index reflecting only post A
# until validation apply targets the newly missing routes.
assert {:ok, post_b} =
Posts.create_post(%{
project_id: project.id,
title: "Second Post",
content: "Second body",
language: "en"
})
assert {:ok, published_b} = Posts.publish_post(post_b.id)
post_b_path = BDS.Generation.post_output_path(published_b)
post_b_file = Path.join([temp_dir, "html", post_b_path])
refute File.exists?(post_b_file)
assert {:ok, report} = BDS.Generation.validate_site(project.id)
assert relative_path_to_url_path(post_b_path) in report.missing_url_paths
assert {:ok, repair} = BDS.Generation.apply_validation(project.id, report)
assert repair.rendered_url_count > 0
# The targeted single-post route is rendered.
assert File.exists?(post_b_file)
# Old-app parity: feed, atom, 404, and the search index are NOT regenerated on apply.
assert File.read!(feed_path) == feed_before
assert File.read!(atom_path) == atom_before
assert File.read!(not_found_path) == not_found_before
assert File.read!(pagefind_path) == pagefind_before
end
test "apply_validation re-renders only the affected post's categories, tags, and single route",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post_a} =
Posts.create_post(%{
project_id: project.id,
title: "Alpha",
content: "Alpha body",
language: "en",
categories: ["notes"],
tags: ["elixir"]
})
assert {:ok, published_a} = Posts.publish_post(post_a.id)
assert {:ok, post_b} =
Posts.create_post(%{
project_id: project.id,
title: "Beta",
content: "Beta body",
language: "en",
categories: ["photos"],
tags: ["erlang"]
})
assert {:ok, published_b} = Posts.publish_post(post_b.id)
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
post_a_path = BDS.Generation.post_output_path(published_a)
post_b_path = BDS.Generation.post_output_path(published_b)
notes_before = generated_updated_at(project.id, "category/notes/index.html")
elixir_before = generated_updated_at(project.id, "tag/elixir/index.html")
photos_before = generated_updated_at(project.id, "category/photos/index.html")
erlang_before = generated_updated_at(project.id, "tag/erlang/index.html")
post_b_before = generated_updated_at(project.id, post_b_path)
File.rm!(Path.join([temp_dir, "html", post_a_path]))
assert {:ok, report} = BDS.Generation.validate_site(project.id)
assert relative_path_to_url_path(post_a_path) in report.missing_url_paths
# Only post A's route is affected; post B must not be flagged as updated.
assert report.updated_post_url_paths == []
assert {:ok, _repair} = BDS.Generation.apply_validation(project.id, report)
# Post A's own single route plus only its category and tag archives are re-rendered.
assert File.exists?(Path.join([temp_dir, "html", post_a_path]))
assert generated_updated_at(project.id, "category/notes/index.html") > notes_before
assert generated_updated_at(project.id, "tag/elixir/index.html") > elixir_before
# Post B's single route and its unrelated category and tag archives are left untouched.
assert generated_updated_at(project.id, post_b_path) == post_b_before
assert generated_updated_at(project.id, "category/photos/index.html") == photos_before
assert generated_updated_at(project.id, "tag/erlang/index.html") == erlang_before
end
test "apply_validation streams one progress message per rewritten URL with a running count",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Progress Post",
content: "Progress body",
language: "en",
categories: ["notes"],
tags: ["elixir"]
})
assert {:ok, published} = Posts.publish_post(post.id)
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
post_path = BDS.Generation.post_output_path(published)
File.rm!(Path.join([temp_dir, "html", post_path]))
assert {:ok, report} = BDS.Generation.validate_site(project.id)
test_pid = self()
assert {:ok, _repair} =
BDS.Generation.apply_validation(project.id, report,
on_progress: fn progress, message ->
send(test_pid, {:progress, progress, message})
end
)
messages = collect_progress_messages([])
post_url = relative_path_to_url_path(post_path)
# The rewritten single-post URL is named in a progress message (like the old app).
assert Enum.any?(messages, fn {_progress, message} -> String.contains?(message, post_url) end)
# Progress messages carry a running "(rewritten/total)" count of URLs.
assert Enum.any?(messages, fn {_progress, message} -> message =~ ~r/\(\d+\/\d+\)/ end)
# Progress advances incrementally rather than jumping straight from 0.0 to 1.0.
fractions =
messages
|> Enum.map(fn {progress, _message} -> progress end)
|> Enum.filter(fn progress -> progress > 0.0 and progress < 1.0 end)
assert fractions != []
end
test "prepare_validation_apply classifies page and day archive routes without section fallback",
%{project: project} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, page} =
Posts.create_post(%{
project_id: project.id,
title: "About",
content: "About body",
language: "en",
categories: ["page"]
})
assert {:ok, published_page} = Posts.publish_post(page.id)
assert {:ok, dated_post} =
Posts.create_post(%{
project_id: project.id,
title: "Day Archive Source",
content: "Day archive body",
language: "en"
})
assert {:ok, published_dated_post} = Posts.publish_post(dated_post.id)
{year, month, day} =
published_dated_post.created_at
|> DateTime.from_unix!(:millisecond)
|> then(&{&1.year, &1.month, &1.day})
assert {:ok, page_preparation} =
BDS.Generation.prepare_validation_apply(project.id, %{
missing_url_paths: ["/#{published_page.slug}"],
updated_post_url_paths: [],
extra_url_paths: []
})
assert page_preparation.sections_to_render == [:core]
assert {:ok, day_preparation} =
BDS.Generation.prepare_validation_apply(project.id, %{
missing_url_paths: [
"/#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}/#{String.pad_leading(Integer.to_string(day), 2, "0")}"
],
updated_post_url_paths: [],
extra_url_paths: []
})
assert day_preparation.sections_to_render == [:date]
end
test "apply_validation_section reports visible progress before section rendering starts", %{
project: project
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "Section Progress Template",
kind: :post,
content: "{{ labels.site_search_label }}: {{ post.title }}"
})
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
published_posts =
Enum.map(1..3, fn index ->
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Section Progress Post #{index}",
content: "Section progress body #{index}",
template_slug: published_template.slug
})
assert {:ok, published_post} = Posts.publish_post(post.id)
published_post
end)
post_url_paths =
Enum.map(
published_posts,
&(BDS.Generation.post_output_path(&1) |> relative_path_to_url_path())
)
parent = self()
on_progress = fn value, message ->
send(parent, {:apply_section_progress, value, message})
end
assert {:ok, _result} =
BDS.Generation.apply_validation_section(
project.id,
%{
missing_url_paths: post_url_paths,
updated_post_url_paths: [],
extra_url_paths: []
},
:single,
on_progress: on_progress
)
assert_receive {:apply_section_progress, +0.0, "Render Single Posts"}
assert_receive {:apply_section_progress, progress, message}
when progress > 0.0 and progress < 1.0 and
is_binary(message)
rendered_path =
Path.join([
project.data_path,
"html",
BDS.Generation.post_output_path(List.first(published_posts))
])
assert File.read!(rendered_path) =~ "Site search: Section Progress Post"
end
test "apply_validation returns an error for unreadable generated files", %{ test "apply_validation returns an error for unreadable generated files", %{
project: project, project: project,
temp_dir: temp_dir temp_dir: temp_dir
@@ -1343,6 +1658,23 @@ defmodule BDS.GenerationTest do
# Pushes generation timestamps and html mtimes far into the past so a # Pushes generation timestamps and html mtimes far into the past so a
# subsequent source-file write is newer by more than the second-granularity # subsequent source-file write is newer by more than the second-granularity
# mtime tolerance, without sleeping across real second boundaries. # mtime tolerance, without sleeping across real second boundaries.
defp collect_progress_messages(acc) do
receive do
{:progress, progress, message} -> collect_progress_messages([{progress, message} | acc])
after
0 -> Enum.reverse(acc)
end
end
defp generated_updated_at(project_id, relative_path) do
BDS.Generation.GeneratedFileHash
|> Repo.get_by(project_id: project_id, relative_path: relative_path)
|> case do
nil -> flunk("expected generated file hash for #{relative_path}")
%{updated_at: updated_at} -> updated_at
end
end
defp backdate_generated_outputs(project_id, temp_dir) do defp backdate_generated_outputs(project_id, temp_dir) do
past_posix = System.os_time(:second) - 120 past_posix = System.os_time(:second) - 120