From 1487c0337e5b071929dd659b5102d890555ba337 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Thu, 2 Jul 2026 18:31:17 +0200 Subject: [PATCH] fix: incremental rendering more on par with bDS --- lib/bds/desktop/shell_commands.ex | 166 ++++++- lib/bds/generation.ex | 583 ++++++++++++++++++++--- lib/bds/generation/outputs.ex | 464 +++++++++++------- lib/bds/generation/validation.ex | 164 +++---- lib/bds/rendering/post_rendering.ex | 28 +- priv/gettext/de/LC_MESSAGES/ui.po | 80 ++++ priv/gettext/en/LC_MESSAGES/ui.po | 80 ++++ priv/gettext/es/LC_MESSAGES/ui.po | 80 ++++ priv/gettext/fr/LC_MESSAGES/ui.po | 80 ++++ priv/gettext/it/LC_MESSAGES/ui.po | 80 ++++ priv/gettext/ui.pot | 293 +++++++----- specs/editor_misc.allium | 7 +- test/bds/desktop/shell_commands_test.exs | 70 +++ test/bds/generation_test.exs | 332 +++++++++++++ 14 files changed, 2003 insertions(+), 504 deletions(-) diff --git a/lib/bds/desktop/shell_commands.ex b/lib/bds/desktop/shell_commands.ex index 80bf887..19716c5 100644 --- a/lib/bds/desktop/shell_commands.ex +++ b/lib/bds/desktop/shell_commands.ex @@ -263,27 +263,39 @@ defmodule BDS.Desktop.ShellCommands do end defp dispatch("apply_site_validation", project, params) do - report = normalize_apply_validation_report(BDS.MapUtils.attr(params, :report, %{})) + validation_report = normalize_apply_validation_report(BDS.MapUtils.attr(params, :report, %{})) + group_id = task_group_id("apply_site_validation") + attrs = %{group_id: group_id, group_name: "Apply Site Validation"} - queue_task( - project, - "apply_site_validation", - "Apply Site Validation", - "Generation", - fn report_fn -> - {:ok, _apply} = - Generation.apply_validation(project.id, report, - on_progress: scaled_progress_reporter(report_fn, 0.0, 0.85) - ) + {:ok, prepare_task} = + Tasks.submit_task( + "Prepare Validation Apply", + fn report -> + {:ok, preparation} = + Generation.prepare_validation_apply(project.id, validation_report, + on_progress: report + ) - {:ok, validation} = - Generation.validate_site(project.id, @site_sections, - on_progress: scaled_progress_reporter(report_fn, 0.85, 1.0) - ) + Map.put(preparation, :project_id, project.id) + end, + attrs + ) - site_validation_result(project.id, validation) - end - ) + Task.start(fn -> + run_apply_validation_sequence(project, validation_report, group_id, attrs, prepare_task.id) + end) + + {:ok, + %{ + kind: "task_queued", + action: "apply_site_validation", + title: "Apply Site Validation", + message: "Apply Site Validation tasks queued", + project_id: project.id, + task_id: prepare_task.id, + task_group_id: group_id, + panel_tab: "tasks" + }} end defp dispatch("metadata_diff", project, _params) do @@ -463,6 +475,124 @@ defmodule BDS.Desktop.ShellCommands do end end + defp run_apply_validation_sequence(project, validation_report, group_id, attrs, prepare_task_id) do + with :ok <- + wait_for_group_phase(group_id, ["Prepare Validation Apply"], @rebuild_phase_timeout), + {:ok, preparation} <- completed_task_result(prepare_task_id), + :ok <- + run_apply_validation_render_tasks( + project, + validation_report, + group_id, + attrs, + preparation + ) do + if validation_apply_changed?(preparation) do + :ok = run_apply_validation_refresh_task(project, group_id, attrs, :calendar) + end + + submit_apply_validation_check_task(project, attrs) + else + _other -> :ok + end + end + + defp run_apply_validation_render_tasks(project, validation_report, group_id, attrs, preparation) do + sections = Map.get(preparation, :sections_to_render, []) + task_names = Enum.map(sections, &apply_validation_section_task_name/1) + + Enum.each(sections, fn section -> + task_name = apply_validation_section_task_name(section) + + {:ok, _task} = + Tasks.submit_task( + task_name, + fn report -> + {:ok, _result} = + Generation.apply_validation_section(project.id, validation_report, section, + on_progress: report + ) + + report.(1.0, apply_validation_section_complete_message(section)) + :ok + end, + attrs + ) + end) + + case task_names do + [] -> :ok + names -> wait_for_group_phase(group_id, names, @rebuild_phase_timeout) + end + end + + defp run_apply_validation_refresh_task(project, group_id, attrs, kind) do + task_name = apply_validation_refresh_task_name(kind) + + {:ok, _task} = + Tasks.submit_task( + task_name, + fn report -> + {:ok, _result} = + Generation.refresh_validation_ancillary_outputs(project.id, kind, on_progress: report) + + report.(1.0, apply_validation_refresh_complete_message(kind)) + :ok + end, + attrs + ) + + wait_for_group_phase(group_id, [task_name], @rebuild_phase_timeout) + end + + defp submit_apply_validation_check_task(project, attrs) do + {:ok, _task} = + Tasks.submit_task( + "Validate Site", + fn report -> + {:ok, validation} = + Generation.validate_site(project.id, @site_sections, on_progress: report) + + site_validation_result(project.id, validation) + end, + attrs + ) + + :ok + end + + defp completed_task_result(task_id) do + case Tasks.get_task(task_id) do + %{status: :completed, result: result} when is_map(result) -> {:ok, result} + %{status: :failed, error: error} -> {:error, error} + _other -> {:error, :task_result_unavailable} + end + end + + defp validation_apply_changed?(preparation) do + Map.get(preparation, :sections_to_render, []) != [] or + Map.get(preparation, :deleted_url_count, 0) > 0 + end + + defp apply_validation_section_task_name(:core), do: "Render Site Core" + defp apply_validation_section_task_name(:single), do: "Render Single Posts" + defp apply_validation_section_task_name(:category), do: "Render Category Archives" + defp apply_validation_section_task_name(:tag), do: "Render Tag Archives" + defp apply_validation_section_task_name(:date), do: "Render Date Archives" + + defp apply_validation_section_complete_message(:core), do: "Render Site Core complete" + defp apply_validation_section_complete_message(:single), do: "Render Single Posts complete" + + defp apply_validation_section_complete_message(:category), + do: "Render Category Archives complete" + + defp apply_validation_section_complete_message(:tag), do: "Render Tag Archives complete" + defp apply_validation_section_complete_message(:date), do: "Render Date Archives complete" + + defp apply_validation_refresh_task_name(:calendar), do: "Regenerate Calendar" + + defp apply_validation_refresh_complete_message(:calendar), do: "Regenerate Calendar complete" + defp translation_fill_enabled?(metadata) do ([metadata.main_language] ++ metadata.blog_languages) |> Enum.map(fn language -> diff --git a/lib/bds/generation.ex b/lib/bds/generation.ex index fb8a6c1..ccdbb30 100644 --- a/lib/bds/generation.ex +++ b/lib/bds/generation.ex @@ -21,6 +21,7 @@ defmodule BDS.Generation do import BDS.Generation.Progress import BDS.Generation.Outputs import BDS.Generation.Data + import BDS.Generation.Renderers, only: [build_list_posts: 3] import BDS.Generation.Validation alias BDS.Generation.GeneratedFileHash @@ -227,62 +228,127 @@ defmodule BDS.Generation do apply_validation(project_id, report, []) end + @spec prepare_validation_apply(String.t(), map(), generation_opts()) :: + {:ok, map()} | {:error, term()} + def prepare_validation_apply(project_id, report, opts \\ []) + + def prepare_validation_apply(project_id, report, opts) + when is_binary(project_id) and is_map(report) and is_list(opts) do + on_progress = callback(opts) + :ok = report_validation_progress(on_progress, 0.0, "Planning validation apply steps...") + + with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do + sections_to_render = validation_apply_sections_to_render(apply_plan.targeted_plan) + project = Projects.get_project!(project_id) + + :ok = report_validation_progress(on_progress, 0.2, "Deleting extra URLs...") + + {deleted_url_count, removed_empty_dir_count} = + delete_extra_validation_paths( + project_id, + project, + Map.get(report, :extra_url_paths, []), + on_progress + ) + + :ok = + report_validation_progress( + on_progress, + 1.0, + "Validation apply prepared" + ) + + {:ok, + %{ + sections_to_render: sections_to_render, + deleted_url_count: deleted_url_count, + removed_empty_dir_count: removed_empty_dir_count + }} + end + end + + @spec apply_validation_section(String.t(), map(), section(), generation_opts()) :: + {:ok, map()} | {:error, term()} + def apply_validation_section(project_id, report, section, opts \\ []) + + def apply_validation_section(project_id, report, section, opts) + when is_binary(project_id) and is_map(report) and section in @core_sections and + is_list(opts) do + on_progress = callback(opts) + :ok = report_validation_progress(on_progress, 0.0, validation_section_task_label(section)) + + with {:ok, apply_plan} <- validation_apply_plan(project_id, [section], report) do + outputs_to_render = + targeted_apply_outputs(apply_plan.plan, apply_plan.targeted_plan, [section]) + + rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress) + + {:ok, %{section: section, rendered_url_count: rendered_url_count}} + end + end + + @spec refresh_validation_ancillary_outputs(String.t(), :calendar, generation_opts()) :: + {:ok, map()} | {:error, term()} + def refresh_validation_ancillary_outputs(project_id, kind, opts \\ []) + + def refresh_validation_ancillary_outputs(project_id, kind, opts) + when is_binary(project_id) and kind in [:calendar] and is_list(opts) do + on_progress = callback(opts) + :ok = report_validation_progress(on_progress, 0.0, ancillary_validation_task_label(kind)) + + with {:ok, plan} <- plan_generation(project_id, @core_sections) do + expected_output_map = refresh_validation_output_map(plan, kind, on_progress) + paths = ancillary_validation_paths(expected_output_map, kind) + label = ancillary_validation_progress_label(kind) + total = length(paths) + + :ok = report_generation_started(on_progress, total, label) + + paths + |> Enum.with_index(1) + |> Enum.each(fn {relative_path, index} -> + _ = + write_generated_file( + project_id, + relative_path, + Map.fetch!(expected_output_map, relative_path) + ) + + :ok = report_generation_progress(on_progress, index, total, label) + end) + + {:ok, %{kind: kind, written_count: length(paths)}} + end + end + + defp refresh_validation_output_map(plan, :calendar, _on_progress) do + data = generation_data(plan) + %{"calendar.json" => BDS.Generation.Sitemap.render_calendar(data.published_list_posts)} + end + @spec apply_validation(String.t(), map(), generation_opts()) :: {:ok, map()} | {:error, term()} def apply_validation(project_id, report, opts) when is_binary(project_id) and is_map(report) and is_list(opts) do on_progress = callback(opts) - with {:ok, plan} <- plan_generation(project_id, @core_sections) do - expected_outputs = build_outputs(plan) - expected_output_map = Map.new(expected_outputs) + with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do + plan = apply_plan.plan project = Projects.get_project!(project_id) - published_posts = list_published_posts(project_id) - targeted_plan = - build_targeted_validation_plan( - plan_validation_paths(report_paths(report), additional_languages(plan)), - published_posts - ) + outputs_to_render = targeted_apply_outputs(plan, apply_plan.targeted_plan, plan.sections) - outputs_to_render = - expected_outputs - |> Enum.filter(fn {relative_path, _content} -> - targeted_output?( - relative_path, - targeted_plan, - plan.language, - additional_languages(plan) - ) - end) - - total_to_render = length(outputs_to_render) - :ok = report_generation_started(on_progress, total_to_render, "validation routes") - - outputs_to_render - |> Enum.with_index(1) - |> Enum.each(fn {{relative_path, content}, index} -> - _ = - write_generated_file(project_id, relative_path, content, - refresh_timestamp_on_unchanged: route_html_path?(relative_path) - ) - - :ok = - report_generation_progress(on_progress, index, total_to_render, "validation routes") - end) + rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress) {deleted_url_count, removed_empty_dir_count} = delete_extra_validation_paths(project_id, project, Map.get(report, :extra_url_paths, [])) if outputs_to_render != [] or deleted_url_count > 0 do - write_ancillary_validation_outputs(project_id, expected_output_map) + {:ok, _} = refresh_validation_ancillary_outputs(project_id, :calendar) end {:ok, %{ - rendered_url_count: - Enum.count(outputs_to_render, fn {relative_path, _content} -> - route_html_path?(relative_path) - end), + rendered_url_count: rendered_url_count, deleted_url_count: deleted_url_count, removed_empty_dir_count: removed_empty_dir_count }} @@ -372,7 +438,7 @@ defmodule BDS.Generation do :ok end - defp build_outputs(plan) do + defp build_outputs(plan, on_output \\ nil) do data = generation_data(plan) published_translations = flattened_generation_translations(data.translations_by_post) translations_by_post_language = translation_lookup_map(published_translations) @@ -419,7 +485,8 @@ defmodule BDS.Generation do build_core_outputs( plan, data.published_list_posts, - localized_list_posts_by_language + localized_list_posts_by_language, + on_output ) else [] @@ -432,7 +499,8 @@ defmodule BDS.Generation do plan.language, data.published_posts, translations_by_post_language, - localized_posts_by_language + localized_posts_by_language, + on_output ) else [] @@ -445,14 +513,15 @@ defmodule BDS.Generation do plan.language, data.published_posts, translations_by_post_language, - localized_posts_by_language + localized_posts_by_language, + on_output ) else [] end archive_outputs = - build_archive_outputs(plan, data.post_index, localized_post_indexes) + build_archive_outputs(plan, data.post_index, localized_post_indexes, on_output) urls = (core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs) @@ -606,6 +675,346 @@ defmodule BDS.Generation do end end + defp validation_apply_plan(project_id, sections, report) do + with {:ok, plan} <- plan_generation(project_id, sections) do + published_posts = list_published_posts(project_id) + + targeted_plan = + build_targeted_validation_plan( + plan_validation_paths(report_paths(report), additional_languages(plan)), + published_posts + ) + + {:ok, %{plan: plan, targeted_plan: targeted_plan}} + end + end + + # Render ONLY the routes targeted by the validation report for the given + # sections, matching the old app's behaviour of filtering the render inputs + # rather than rendering the whole site and discarding the surplus. Falls back + # to a full section render only when a report path could not be classified. + defp targeted_apply_outputs(plan, targeted_plan, sections) do + if fallback_validation_section_render?(targeted_plan) do + Enum.filter(build_outputs(plan), fn {relative_path, _content} -> + path_section(relative_path) in sections + end) + else + render_data = validation_render_data(plan) + + Enum.flat_map(sections, fn section -> + build_targeted_section_outputs(plan, render_data, targeted_plan, section) + end) + end + end + + # Write the targeted outputs, streaming one progress message per rewritten URL + # (exactly like the old app's `Generated /url` reporting) so the task shows the + # actual URLs and advances by the number of URLs rewritten. + defp write_validation_outputs(project_id, outputs, on_progress) do + route_total = Enum.count(outputs, fn {relative_path, _content} -> route_html_path?(relative_path) end) + + :ok = report_validation_progress(on_progress, 0.0, validation_rewrite_started_message(route_total)) + + {rendered, _total} = + Enum.reduce(outputs, {0, route_total}, fn {relative_path, content}, {rendered, total} -> + _ = + write_generated_file(project_id, relative_path, content, + refresh_timestamp_on_unchanged: route_html_path?(relative_path) + ) + + if route_html_path?(relative_path) do + rewritten = rendered + 1 + progress = if total > 0, do: rewritten / total, else: 1.0 + + :ok = + report_validation_progress( + on_progress, + progress, + "Rewrote #{relative_path_to_url_path(relative_path)} (#{rewritten}/#{total})" + ) + + {rewritten, total} + else + {rendered, total} + end + end) + + rendered + end + + defp validation_rewrite_started_message(0), do: "No URLs to rewrite" + defp validation_rewrite_started_message(1), do: "Rewriting 1 URL..." + defp validation_rewrite_started_message(total), do: "Rewriting #{total} URLs..." + + defp validation_render_data(plan) do + data = generation_data(plan) + + translations_by_post_language = + data.translations_by_post + |> flattened_generation_translations() + |> translation_lookup_map() + + translatable_posts = + Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate))) + + translatable_list_posts = + Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate))) + + localized_posts_by_language = + Map.new(additional_languages(plan), fn language -> + {language, + resolve_posts_for_language( + translatable_posts, + language, + translations_by_post_language, + plan.language + )} + end) + + localized_list_posts_by_language = + Map.new(additional_languages(plan), fn language -> + {language, + resolve_posts_for_language( + translatable_list_posts, + language, + translations_by_post_language, + plan.language + )} + end) + + localized_post_indexes = + Map.new(localized_list_posts_by_language, fn {language, posts} -> + {language, build_generation_post_index(posts)} + end) + + %{ + data: data, + translations_by_post_language: translations_by_post_language, + localized_posts_by_language: localized_posts_by_language, + localized_list_posts_by_language: localized_list_posts_by_language, + localized_post_indexes: localized_post_indexes + } + end + + defp build_targeted_section_outputs(plan, render_data, targeted_plan, :core) do + data = render_data.data + + main_root = + if targeted_plan.request_root_routes do + posts = build_list_posts(plan.base_url, data.published_list_posts, nil) + build_root_outputs(plan, plan.language, posts) + else + [] + end + + language_root = + Enum.flat_map(additional_languages(plan), fn language -> + language_plan = language_targeted_plan(targeted_plan, language) + + if language_plan.request_root_routes do + source = Map.get(render_data.localized_list_posts_by_language, language, []) + prefix = route_language(plan.language, language) + posts = build_list_posts(plan.base_url, source, prefix) + build_root_outputs(plan, language, posts) + else + [] + end + end) + + main_page_posts = + Enum.filter( + data.published_posts, + &MapSet.member?(targeted_plan.requested_page_slugs, &1.slug) + ) + + localized_page_posts = + Map.new(additional_languages(plan), fn language -> + language_plan = language_targeted_plan(targeted_plan, language) + source = Map.get(render_data.localized_posts_by_language, language, []) + {language, Enum.filter(source, &MapSet.member?(language_plan.requested_page_slugs, &1.slug))} + end) + + page_outputs = + build_page_outputs( + plan.project_id, + plan.language, + main_page_posts, + render_data.translations_by_post_language, + localized_page_posts + ) + + main_root ++ language_root ++ page_outputs + end + + defp build_targeted_section_outputs(plan, render_data, targeted_plan, :single) do + data = render_data.data + + main_posts = + filter_route_posts(data.published_posts, targeted_plan.requested_post_routes) + + localized_posts = + Map.new(additional_languages(plan), fn language -> + language_plan = language_targeted_plan(targeted_plan, language) + source = Map.get(render_data.localized_posts_by_language, language, []) + {language, filter_route_posts(source, language_plan.requested_post_routes)} + end) + + build_single_outputs( + plan.project_id, + plan.language, + main_posts, + render_data.translations_by_post_language, + localized_posts + ) + end + + defp build_targeted_section_outputs(plan, render_data, targeted_plan, :category) do + build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg, + index, + language_plan, + languages -> + build_category_outputs( + plan_arg, + filter_collection_by_segment(index.posts_by_category, language_plan.requested_category_slugs), + languages + ) + end) + end + + defp build_targeted_section_outputs(plan, render_data, targeted_plan, :tag) do + build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg, + index, + language_plan, + languages -> + build_tag_outputs( + plan_arg, + filter_collection_by_segment(index.posts_by_tag, language_plan.requested_tag_slugs), + languages + ) + end) + end + + defp build_targeted_section_outputs(plan, render_data, targeted_plan, :date) do + build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg, + index, + language_plan, + languages -> + build_date_outputs(plan_arg, filter_date_index(index, language_plan), languages) + end) + end + + defp build_targeted_archive_outputs(plan, render_data, targeted_plan, builder) do + main_outputs = builder.(plan, render_data.data.post_index, targeted_plan, [plan.language]) + + language_outputs = + Enum.flat_map(additional_languages(plan), fn language -> + language_plan = language_targeted_plan(targeted_plan, language) + index = Map.get(render_data.localized_post_indexes, language, empty_generation_post_index()) + builder.(plan, index, language_plan, [language]) + end) + + main_outputs ++ language_outputs + end + + defp language_targeted_plan(targeted_plan, language) do + Map.get(targeted_plan.language_plans, language, empty_validation_path_plan()) + end + + defp filter_route_posts(posts, route_keys) do + Enum.filter(posts, fn post -> + {year, month, day} = local_date_parts!(post.created_at) + MapSet.member?(route_keys, route_key(year, month, day, post.slug)) + end) + end + + defp filter_collection_by_segment(collection, segment_slugs) do + collection + |> Enum.filter(fn {key, _posts} -> + MapSet.member?(segment_slugs, archive_route_segment(key)) + end) + |> Map.new() + end + + defp filter_date_index(index, language_plan) do + %{ + posts_by_year: filter_map_by_keys(index.posts_by_year, language_plan.requested_years), + posts_by_year_month: + filter_map_by_keys(index.posts_by_year_month, language_plan.requested_year_months), + posts_by_year_month_day: + filter_map_by_keys(index.posts_by_year_month_day, language_plan.requested_year_month_days) + } + end + + defp filter_map_by_keys(map, keys) do + map + |> Enum.filter(fn {key, _value} -> MapSet.member?(keys, key) end) + |> Map.new() + end + + defp empty_generation_post_index do + %{ + posts_by_category: %{}, + posts_by_tag: %{}, + posts_by_year: %{}, + posts_by_year_month: %{}, + posts_by_year_month_day: %{} + } + end + + defp validation_apply_sections_to_render(targeted_plan) do + if fallback_validation_section_render?(targeted_plan) do + [:category, :tag, :date, :core, :single] + else + targeted_plan + |> validation_apply_section_plans() + |> Enum.reduce([], &append_validation_apply_sections/2) + end + end + + defp fallback_validation_section_render?(%{requires_fallback_section_render: true}), do: true + + defp fallback_validation_section_render?(%{language_plans: language_plans}) do + Enum.any?(Map.values(language_plans), &fallback_validation_section_render?/1) + end + + defp validation_apply_section_plans(targeted_plan) do + [targeted_plan | Map.values(Map.get(targeted_plan, :language_plans, %{}))] + end + + defp append_validation_apply_sections(plan, sections) do + sections + |> maybe_append_validation_apply_section(:core, validation_apply_core_section?(plan)) + |> maybe_append_validation_apply_section( + :single, + not MapSet.equal?(plan.requested_post_routes, MapSet.new()) + ) + |> maybe_append_validation_apply_section( + :category, + not MapSet.equal?(plan.requested_category_slugs, MapSet.new()) + ) + |> maybe_append_validation_apply_section( + :tag, + not MapSet.equal?(plan.requested_tag_slugs, MapSet.new()) + ) + |> maybe_append_validation_apply_section(:date, validation_apply_date_section?(plan)) + end + + defp validation_apply_core_section?(plan) do + plan.request_root_routes or not MapSet.equal?(plan.requested_page_slugs, MapSet.new()) + end + + defp validation_apply_date_section?(plan) do + not MapSet.equal?(plan.requested_years, MapSet.new()) or + not MapSet.equal?(plan.requested_year_months, MapSet.new()) or + not MapSet.equal?(plan.requested_year_month_days, MapSet.new()) + end + + defp maybe_append_validation_apply_section(sections, _section, false), do: sections + + defp maybe_append_validation_apply_section(sections, section, true) do + if section in sections, do: sections, else: sections ++ [section] + end + defp path_section(relative_path) do segments = String.split(relative_path, "/", trim: true) @@ -698,50 +1107,70 @@ defmodule BDS.Generation do :ok end + defp validation_section_task_label(:core), do: "Render Site Core" + defp validation_section_task_label(:single), do: "Render Single Posts" + defp validation_section_task_label(:category), do: "Render Category Archives" + defp validation_section_task_label(:tag), do: "Render Tag Archives" + defp validation_section_task_label(:date), do: "Render Date Archives" + + defp ancillary_validation_progress_label(:calendar), do: "calendar files" + + defp ancillary_validation_task_label(:calendar), do: "Regenerate Calendar" + defp delete_extra_validation_paths(project_id, project, extra_url_paths) do - Enum.reduce(extra_url_paths, {0, 0}, fn url_path, {deleted_count, removed_dir_count} -> + delete_extra_validation_paths(project_id, project, extra_url_paths, nil) + end + + defp delete_extra_validation_paths(project_id, project, extra_url_paths, on_progress) do + total = length(extra_url_paths) + + if total == 0 do + :ok = report_validation_progress(on_progress, 0.5, "No extra URLs to delete") + end + + extra_url_paths + |> Enum.with_index(1) + |> Enum.reduce({0, 0}, fn {url_path, index}, {deleted_count, removed_dir_count} -> relative_path = url_path_to_relative_index_path(url_path) full_path = output_path(project, relative_path) - case File.rm(full_path) do - :ok -> - Repo.delete_all( - from generated_file in GeneratedFileHash, - where: - generated_file.project_id == ^project_id and - generated_file.relative_path == ^relative_path - ) + result = + case File.rm(full_path) do + :ok -> + Repo.delete_all( + from generated_file in GeneratedFileHash, + where: + generated_file.project_id == ^project_id and + generated_file.relative_path == ^relative_path + ) - {pruned_count, _last_dir} = - prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, "")) + {pruned_count, _last_dir} = + prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, "")) - {deleted_count + 1, removed_dir_count + pruned_count} + {deleted_count + 1, removed_dir_count + pruned_count} - {:error, :enoent} -> - {deleted_count, removed_dir_count} + {:error, :enoent} -> + {deleted_count, removed_dir_count} - {:error, _reason} -> - {deleted_count, removed_dir_count} - end + {:error, _reason} -> + {deleted_count, removed_dir_count} + end + + :ok = + report_validation_progress( + on_progress, + 0.2 + 0.3 * index / max(total, 1), + "Deleted #{index}/#{total} extra URLs" + ) + + result end) end - defp write_ancillary_validation_outputs(project_id, expected_output_map) do - ancillary_paths = - Enum.filter(Map.keys(expected_output_map), fn relative_path -> - relative_path == "calendar.json" or String.contains?(relative_path, "pagefind/") - end) - - Enum.each(ancillary_paths, fn relative_path -> - _ = - write_generated_file( - project_id, - relative_path, - Map.fetch!(expected_output_map, relative_path) - ) - end) - - :ok + defp ancillary_validation_paths(expected_output_map, :calendar) do + expected_output_map + |> Map.keys() + |> Enum.filter(&(&1 == "calendar.json")) end defp output_path(project, relative_path) do diff --git a/lib/bds/generation/outputs.ex b/lib/bds/generation/outputs.ex index adaf975..dcdf963 100644 --- a/lib/bds/generation/outputs.ex +++ b/lib/bds/generation/outputs.ex @@ -7,6 +7,8 @@ defmodule BDS.Generation.Outputs do alias BDS.Rendering.TemplateSelection + @type output_callback :: ({String.t(), iodata()} -> any()) | nil + @spec additional_languages(map()) :: [String.t()] def additional_languages(plan) do Enum.reject(plan.blog_languages, &(&1 == plan.language)) @@ -149,16 +151,17 @@ defmodule BDS.Generation.Outputs do end) end - @spec build_archive_outputs(map(), map(), map()) :: [{String.t(), iodata()}] - def build_archive_outputs(plan, post_index, localized_post_indexes) do + @spec build_archive_outputs(map(), map(), map(), output_callback()) :: [{String.t(), iodata()}] + def build_archive_outputs(plan, post_index, localized_post_indexes, on_output \\ nil) do category_outputs = if :category in plan.sections do - build_category_outputs(plan, post_index.posts_by_category, [plan.language]) ++ + build_category_outputs(plan, post_index.posts_by_category, [plan.language], on_output) ++ Enum.flat_map(additional_languages(plan), fn language -> build_category_outputs( plan, Map.get(localized_post_indexes, language, %{posts_by_category: %{}}).posts_by_category, - [language] + [language], + on_output ) end) else @@ -167,12 +170,13 @@ defmodule BDS.Generation.Outputs do tag_outputs = if :tag in plan.sections do - build_tag_outputs(plan, post_index.posts_by_tag, [plan.language]) ++ + build_tag_outputs(plan, post_index.posts_by_tag, [plan.language], on_output) ++ Enum.flat_map(additional_languages(plan), fn language -> build_tag_outputs( plan, Map.get(localized_post_indexes, language, %{posts_by_tag: %{}}).posts_by_tag, - [language] + [language], + on_output ) end) else @@ -181,7 +185,7 @@ defmodule BDS.Generation.Outputs do date_outputs = if :date in plan.sections do - build_date_outputs(plan, post_index, [plan.language]) ++ + build_date_outputs(plan, post_index, [plan.language], on_output) ++ Enum.flat_map(additional_languages(plan), fn language -> build_date_outputs( plan, @@ -190,7 +194,8 @@ defmodule BDS.Generation.Outputs do language, %{posts_by_year: %{}, posts_by_year_month: %{}, posts_by_year_month_day: %{}} ), - [language] + [language], + on_output ) end) else @@ -200,8 +205,10 @@ defmodule BDS.Generation.Outputs do category_outputs ++ tag_outputs ++ date_outputs end - @spec build_category_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}] - def build_category_outputs(plan, posts_by_category, languages) do + @spec build_category_outputs(map(), map(), [String.t()], output_callback()) :: [ + {String.t(), iodata()} + ] + def build_category_outputs(plan, posts_by_category, languages, on_output \\ nil) do Enum.flat_map(posts_by_category, fn {category, posts} -> paginated_posts = Enum.chunk_every(posts, max(plan.max_posts_per_page, 1)) category_slug = archive_route_segment(category) @@ -240,34 +247,46 @@ defmodule BDS.Generation.Outputs do ) } - { - archive_path( - route_language(plan.language, language), - ["category", category_slug], - page_number - ), - render_archive_page(plan, category, page_posts, language, "category", pagination) - } + output = + { + archive_path( + route_language(plan.language, language), + ["category", category_slug], + page_number + ), + render_archive_page(plan, category, page_posts, language, "category", pagination) + } + + report_output(output, on_output) end) end) end) end - @spec build_tag_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}] - def build_tag_outputs(plan, posts_by_tag, languages) do + @spec build_tag_outputs(map(), map(), [String.t()], output_callback()) :: [ + {String.t(), iodata()} + ] + def build_tag_outputs(plan, posts_by_tag, languages, on_output \\ nil) do Enum.flat_map(posts_by_tag, fn {tag, posts} -> tag_slug = archive_route_segment(tag) - build_paginated_archive_outputs(plan, languages, ["tag", tag_slug], posts, fn page_posts, - language, - pagination -> - render_archive_page(plan, tag, page_posts, language, "tag", pagination) - end) + build_paginated_archive_outputs( + plan, + languages, + ["tag", tag_slug], + posts, + fn page_posts, language, pagination -> + render_archive_page(plan, tag, page_posts, language, "tag", pagination) + end, + on_output + ) end) end - @spec build_date_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}] - def build_date_outputs(plan, post_index, languages) do + @spec build_date_outputs(map(), map(), [String.t()], output_callback()) :: [ + {String.t(), iodata()} + ] + def build_date_outputs(plan, post_index, languages, on_output \\ nil) do year_outputs = Enum.flat_map(post_index.posts_by_year, fn {year, posts} -> build_paginated_archive_outputs( @@ -284,7 +303,8 @@ defmodule BDS.Generation.Outputs do language, pagination ) - end + end, + on_output ) end) @@ -292,59 +312,73 @@ defmodule BDS.Generation.Outputs do Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} -> [year, month] = String.split(year_month, "/", parts: 2) - build_paginated_archive_outputs(plan, languages, [year, month], posts, fn page_posts, - language, - pagination -> - render_date_archive_page( - plan, - "#{year}-#{month}", - %{kind: "month", year: String.to_integer(year), month: String.to_integer(month)}, - page_posts, - language, - pagination - ) - end) + build_paginated_archive_outputs( + plan, + languages, + [year, month], + posts, + fn page_posts, language, pagination -> + render_date_archive_page( + plan, + "#{year}-#{month}", + %{kind: "month", year: String.to_integer(year), month: String.to_integer(month)}, + page_posts, + language, + pagination + ) + end, + on_output + ) end) day_outputs = Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} -> [year, month, day] = String.split(year_month_day, "/", parts: 3) - build_paginated_archive_outputs(plan, languages, [year, month, day], posts, fn page_posts, - language, - pagination -> - render_date_archive_page( - plan, - "#{year}-#{month}-#{day}", - %{ - kind: "day", - year: String.to_integer(year), - month: String.to_integer(month), - day: String.to_integer(day) - }, - page_posts, - language, - pagination - ) - end) + build_paginated_archive_outputs( + plan, + languages, + [year, month, day], + posts, + fn page_posts, language, pagination -> + render_date_archive_page( + plan, + "#{year}-#{month}-#{day}", + %{ + kind: "day", + year: String.to_integer(year), + month: String.to_integer(month), + day: String.to_integer(day) + }, + page_posts, + language, + pagination + ) + end, + on_output + ) end) year_outputs ++ month_outputs ++ day_outputs end - @spec build_core_outputs(map(), [map()], map()) :: [{String.t(), iodata()}] - def build_core_outputs(plan, published_posts, localized_posts_by_language) do + @spec build_core_outputs(map(), [map()], map(), output_callback()) :: [{String.t(), iodata()}] + def build_core_outputs(plan, published_posts, localized_posts_by_language, on_output \\ nil) do language = plan.language additional_languages = Enum.reject(plan.blog_languages, &(&1 == language)) main_posts = build_list_posts(plan.base_url, published_posts, nil) - build_root_outputs(plan, language, main_posts) ++ + main_static_outputs = [ {"404.html", render_not_found_output(plan, language)}, {"feed.xml", render_feed(plan, language, published_posts)}, {"atom.xml", render_atom(plan, language, published_posts)}, {"calendar.json", render_calendar(published_posts)} - ] ++ + ] + |> Enum.map(&report_output(&1, on_output)) + + build_root_outputs(plan, language, main_posts, on_output) ++ + main_static_outputs ++ Enum.flat_map(additional_languages, fn localized_language -> localized_prefix = route_language(plan.language, localized_language) localized_source_posts = Map.get(localized_posts_by_language, localized_language, []) @@ -352,19 +386,20 @@ defmodule BDS.Generation.Outputs do localized_posts = build_list_posts(plan.base_url, localized_source_posts, localized_prefix) - build_root_outputs(plan, localized_language, localized_posts) ++ - [ - {Path.join(localized_language, "404.html"), - render_not_found_output(plan, localized_language)}, - {Path.join(localized_language, "feed.xml"), - render_feed(plan, localized_language, localized_source_posts)}, - {Path.join(localized_language, "atom.xml"), - render_atom(plan, localized_language, localized_source_posts)} - ] + (build_root_outputs(plan, localized_language, localized_posts, on_output) ++ + [ + {Path.join(localized_language, "404.html"), + render_not_found_output(plan, localized_language)}, + {Path.join(localized_language, "feed.xml"), + render_feed(plan, localized_language, localized_source_posts)}, + {Path.join(localized_language, "atom.xml"), + render_atom(plan, localized_language, localized_source_posts)} + ]) + |> Enum.map(&report_output(&1, on_output)) end) end - @spec build_page_outputs(String.t(), String.t(), [map()], map(), map()) :: [ + @spec build_page_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) :: [ {String.t(), iodata()} ] def build_page_outputs( @@ -372,39 +407,44 @@ defmodule BDS.Generation.Outputs do main_language, published_posts, translations_by_post_language, - localized_posts_by_language + localized_posts_by_language, + on_output \\ nil ) do page_outputs = published_posts |> Enum.filter(&("page" in (&1.categories || []))) |> Enum.map(fn post -> canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post) + render_language = effective_render_language(canonical_variant.language, main_language) body = load_body(project_id, canonical_variant.file_path, canonical_variant.content) effective_slug = effective_template_slug(project_id, post) - {page_output_path(post.slug, nil), - render_post_output( - project_id, - effective_slug, - %{ - id: canonical_variant.id, - title: canonical_variant.title, - content: body, - slug: post.slug, - language: canonical_variant.language, - excerpt: canonical_variant.excerpt, - _post_record: canonical_variant - }, - fn -> - render_post_page( - canonical_variant.title, - body, - post.slug, - canonical_variant.language - ) - end - )} + output = + {page_output_path(post.slug, nil), + render_post_output( + project_id, + effective_slug, + %{ + id: canonical_variant.id, + title: canonical_variant.title, + content: body, + slug: post.slug, + language: render_language, + excerpt: canonical_variant.excerpt, + _post_record: canonical_variant + }, + fn -> + render_post_page( + canonical_variant.title, + body, + post.slug, + render_language + ) + end + )} + + report_output(output, on_output) end) translation_page_outputs = @@ -413,33 +453,39 @@ defmodule BDS.Generation.Outputs do posts |> Enum.filter(&("page" in (&1.categories || []))) |> Enum.map(fn post -> + render_language = effective_render_language(post.language, language) body = load_body(project_id, post.file_path, post.content) effective_slug = effective_template_slug(project_id, post) - {page_output_path(post.slug, language), - render_post_output( - project_id, - effective_slug, - %{ - id: post.id, - title: post.title, - content: body, - slug: post.slug, - language: post.language, - excerpt: post.excerpt, - _post_record: post - }, - fn -> render_post_page(post.title, body, post.slug, post.language) end - )} + output = + {page_output_path(post.slug, language), + render_post_output( + project_id, + effective_slug, + %{ + id: post.id, + title: post.title, + content: body, + slug: post.slug, + language: render_language, + excerpt: post.excerpt, + _post_record: post + }, + fn -> render_post_page(post.title, body, post.slug, render_language) end + )} + + report_output(output, on_output) end) end) page_outputs ++ translation_page_outputs end - @spec build_root_outputs(map(), String.t(), [map()]) :: [{String.t(), iodata()}] - def build_root_outputs(plan, language, posts) do + @spec build_root_outputs(map(), String.t(), [map()], output_callback()) :: [ + {String.t(), iodata()} + ] + def build_root_outputs(plan, language, posts, on_output \\ nil) do total_items = length(posts) total_pages = page_count(total_items, plan.max_posts_per_page) @@ -449,30 +495,46 @@ defmodule BDS.Generation.Outputs do |> Enum.map(fn {page_posts, page_number} -> route_language = route_language(plan.language, language) - {root_output_path(route_language, page_number), - render_list_output( - plan, - language, - plan.project_name, - page_posts, - %{kind: "core"}, - pagination_for_page( - page_number, - total_pages, - total_items, - plan.max_posts_per_page, - route_language, - [] - ), - fn -> render_home(plan, language) end - )} + output = + {root_output_path(route_language, page_number), + render_list_output( + plan, + language, + plan.project_name, + page_posts, + %{kind: "core"}, + pagination_for_page( + page_number, + total_pages, + total_items, + plan.max_posts_per_page, + route_language, + [] + ), + fn -> render_home(plan, language) end + )} + + report_output(output, on_output) end) end - @spec build_paginated_archive_outputs(map(), [String.t()], [String.t()], [map()], (... -> - iodata())) :: + @spec build_paginated_archive_outputs( + map(), + [String.t()], + [String.t()], + [map()], + (... -> iodata()), + output_callback() + ) :: [{String.t(), iodata()}] - def build_paginated_archive_outputs(plan, languages, segments, posts, render_fun) do + def build_paginated_archive_outputs( + plan, + languages, + segments, + posts, + render_fun, + on_output \\ nil + ) do total_items = length(posts) total_pages = page_count(total_items, plan.max_posts_per_page) @@ -483,87 +545,100 @@ defmodule BDS.Generation.Outputs do Enum.map(languages, fn language -> route_language = route_language(plan.language, language) - {archive_path(route_language, segments, page_number), - render_fun.( - page_posts, - language, - pagination_for_page( - page_number, - total_pages, - total_items, - plan.max_posts_per_page, - route_language, - segments - ) - )} + output = + {archive_path(route_language, segments, page_number), + render_fun.( + page_posts, + language, + pagination_for_page( + page_number, + total_pages, + total_items, + plan.max_posts_per_page, + route_language, + segments + ) + )} + + report_output(output, on_output) end) end) end - @spec build_single_outputs(String.t(), String.t(), [map()], map(), map()) :: [ - {String.t(), iodata()} - ] + @spec build_single_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) :: + [ + {String.t(), iodata()} + ] def build_single_outputs( project_id, main_language, published_posts, translations_by_post_language, - localized_posts_by_language + localized_posts_by_language, + on_output \\ nil ) do post_outputs = Enum.map(published_posts, fn post -> canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post) + render_language = effective_render_language(canonical_variant.language, main_language) body = load_body(project_id, canonical_variant.file_path, canonical_variant.content) effective_slug = effective_template_slug(project_id, post) - {post_output_path(post), - render_post_output( - project_id, - effective_slug, - %{ - id: canonical_variant.id, - title: canonical_variant.title, - content: body, - slug: post.slug, - language: canonical_variant.language, - excerpt: canonical_variant.excerpt, - _post_record: canonical_variant - }, - fn -> - render_post_page( - canonical_variant.title, - body, - post.slug, - canonical_variant.language - ) - end - )} + output = + {post_output_path(post), + render_post_output( + project_id, + effective_slug, + %{ + id: canonical_variant.id, + title: canonical_variant.title, + content: body, + slug: post.slug, + language: render_language, + excerpt: canonical_variant.excerpt, + _post_record: canonical_variant + }, + fn -> + render_post_page( + canonical_variant.title, + body, + post.slug, + render_language + ) + end + )} + + report_output(output, on_output) end) translation_outputs = localized_posts_by_language |> Enum.flat_map(fn {language, posts} -> Enum.map(posts, fn post -> + render_language = effective_render_language(post.language, language) body = load_body(project_id, post.file_path, post.content) effective_slug = effective_template_slug(project_id, post) - {post_output_path(post, language), - render_post_output( - project_id, - effective_slug, - %{ - id: post.id, - title: post.title, - content: body, - slug: post.slug, - language: post.language, - excerpt: post.excerpt, - _post_record: post - }, - fn -> render_post_page(post.title, body, post.slug, post.language) end - )} + output = + {post_output_path(post, language), + render_post_output( + project_id, + effective_slug, + %{ + id: post.id, + title: post.title, + content: body, + slug: post.slug, + language: render_language, + excerpt: post.excerpt, + _post_record: post + }, + fn -> render_post_page(post.title, body, post.slug, render_language) end + )} + + report_output(output, on_output) end) end) @@ -583,4 +658,23 @@ defmodule BDS.Generation.Outputs do ) end end + + defp effective_render_language(language, _fallback_language) + when is_binary(language) and language != "" do + language + end + + defp effective_render_language(_language, fallback_language) + when is_binary(fallback_language) and fallback_language != "" do + fallback_language + end + + defp effective_render_language(_language, _fallback_language), do: "en" + + defp report_output(output, nil), do: output + + defp report_output(output, on_output) when is_function(on_output, 1) do + on_output.(output) + output + end end diff --git a/lib/bds/generation/validation.ex b/lib/bds/generation/validation.ex index 3e4c400..fec33cc 100644 --- a/lib/bds/generation/validation.ex +++ b/lib/bds/generation/validation.ex @@ -245,7 +245,9 @@ defmodule BDS.Generation.Validation do requested_tag_slugs: MapSet.new(), requested_years: MapSet.new(), requested_year_months: MapSet.new(), + requested_year_month_days: MapSet.new(), requested_post_routes: [], + requested_page_slugs: MapSet.new(), language_plans: %{} } end @@ -277,28 +279,38 @@ defmodule BDS.Generation.Validation do ) nil -> - case Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) do - [_, year, month] -> - update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}")) - - nil -> - case Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path) do - [_, year] -> - update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year))) - - nil -> - if path == "/" or Regex.match?(~r|^/page/\d+$|, path) do - %{plan | request_root_routes: true} - else - %{plan | requires_fallback_section_render: true} - end - end - end + classify_archive_or_page_validation_path(path, plan) end end end end + defp classify_archive_or_page_validation_path(path, plan) do + cond do + Regex.match?(~r|^/(\d{4})/(\d{2})/(\d{2})(?:/page/\d+)?$|, path) -> + [_, year, month, day] = Regex.run(~r|^/(\d{4})/(\d{2})/(\d{2})(?:/page/\d+)?$|, path) + update_in(plan.requested_year_month_days, &MapSet.put(&1, "#{year}/#{month}/#{day}")) + + Regex.match?(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) -> + [_, year, month] = Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) + update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}")) + + Regex.match?(~r|^/(\d{4})(?:/page/\d+)?$|, path) -> + [_, year] = Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path) + update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year))) + + path == "/" or Regex.match?(~r|^/page/\d+$|, path) -> + %{plan | request_root_routes: true} + + Regex.match?(~r|^/([^/]+)$|, path) -> + [_, slug] = Regex.run(~r|^/([^/]+)$|, path) + update_in(plan.requested_page_slugs, &MapSet.put(&1, slug)) + + true -> + %{plan | requires_fallback_section_render: true} + end + end + @spec build_targeted_validation_plan(map(), [map()]) :: map() def build_targeted_validation_plan(initial_plan, published_posts) do if initial_plan.requires_fallback_section_render do @@ -334,10 +346,14 @@ defmodule BDS.Generation.Validation do [:requested_year_months], &MapSet.put(&1, route_month_key(route.year, route.month)) ) + |> update_in( + [:requested_year_month_days], + &MapSet.put(&1, route_day_key(route.year, route.month, route.day)) + ) |> Map.put(:request_root_routes, true) post -> - {year, month, _day} = local_date_parts!(post.created_at) + {year, month, day} = local_date_parts!(post.created_at) acc |> update_in([:requested_category_slugs], fn set -> @@ -355,10 +371,15 @@ defmodule BDS.Generation.Validation do [:requested_year_months], &MapSet.put(&1, route_month_key(year, month)) ) + |> update_in( + [:requested_year_month_days], + &MapSet.put(&1, route_day_key(year, month, day)) + ) |> Map.put(:request_root_routes, true) end end ) + |> cascade_requested_date_archives() language_plans = initial_plan.language_plans @@ -384,7 +405,8 @@ defmodule BDS.Generation.Validation do post.slug == route.slug and year == route.year and month == route.month and day == route.day end - defp route_key(year, month, day, slug) do + @spec route_key(integer(), integer(), integer(), String.t()) :: String.t() + def route_key(year, month, day, slug) do "#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}/#{String.pad_leading(Integer.to_string(day), 2, "0")}/#{slug}" end @@ -392,6 +414,33 @@ defmodule BDS.Generation.Validation do "#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}" end + defp route_day_key(year, month, day) do + "#{route_month_key(year, month)}/#{String.pad_leading(Integer.to_string(day), 2, "0")}" + end + + defp cascade_requested_date_archives(plan) do + plan + |> cascade_requested_year_month_days() + |> cascade_requested_year_months() + end + + defp cascade_requested_year_month_days(plan) do + Enum.reduce(plan.requested_year_month_days, plan, fn year_month_day, acc -> + [year, month, _day] = String.split(year_month_day, "/", parts: 3) + + acc + |> update_in([:requested_year_months], &MapSet.put(&1, "#{year}/#{month}")) + |> update_in([:requested_years], &MapSet.put(&1, String.to_integer(year))) + end) + end + + defp cascade_requested_year_months(plan) do + Enum.reduce(plan.requested_year_months, plan, fn year_month, acc -> + [year | _rest] = String.split(year_month, "/", parts: 2) + update_in(acc.requested_years, &MapSet.put(&1, String.to_integer(year))) + end) + end + defp extract_language_path(path, additional_languages) do case Regex.run(~r|^/([a-z]{2,3})(/.*)?$|, path) do [_, language, suffix] -> @@ -413,83 +462,6 @@ defmodule BDS.Generation.Validation do end end - @spec targeted_output?(String.t(), map(), String.t() | nil, [String.t()]) :: boolean() - def targeted_output?(relative_path, targeted_plan, main_language, additional_languages) do - {language, stripped_path} = - extract_relative_output_language(relative_path, additional_languages) - - plan = - case language do - nil -> targeted_plan - value -> Map.get(targeted_plan.language_plans, value, empty_validation_path_plan()) - end - - targeted_output_for_plan?(stripped_path, plan, main_language == language or is_nil(language)) - end - - defp extract_relative_output_language(relative_path, additional_languages) do - segments = String.split(relative_path, "/", trim: true) - - case segments do - [language | rest] -> - if language in additional_languages do - {language, Path.join(rest)} - else - {nil, relative_path} - end - - _other -> - {nil, relative_path} - end - end - - defp targeted_output_for_plan?( - _relative_path, - %{requires_fallback_section_render: true}, - _main? - ), - do: true - - defp targeted_output_for_plan?(relative_path, plan, _main?) do - cond do - relative_path in ["index.html", "404.html", "feed.xml", "atom.xml"] -> - plan.request_root_routes - - Regex.match?(~r|^category/([^/]+)(?:/page/\d+)?/index\.html$|, relative_path) -> - [_, slug] = Regex.run(~r|^category/([^/]+)(?:/page/\d+)?/index\.html$|, relative_path) - MapSet.member?(plan.requested_category_slugs, slug) - - Regex.match?(~r|^tag/([^/]+)/index\.html$|, relative_path) -> - [_, slug] = Regex.run(~r|^tag/([^/]+)/index\.html$|, relative_path) - MapSet.member?(plan.requested_tag_slugs, slug) - - Regex.match?(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path) -> - [_, year, month, day, slug] = - Regex.run(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path) - - MapSet.member?( - plan.requested_post_routes, - route_key( - String.to_integer(year), - String.to_integer(month), - String.to_integer(day), - slug - ) - ) - - Regex.match?(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path) -> - [_, year, month] = Regex.run(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path) - MapSet.member?(plan.requested_year_months, "#{year}/#{month}") - - Regex.match?(~r|^(\d{4})/index\.html$|, relative_path) -> - [_, year] = Regex.run(~r|^(\d{4})/index\.html$|, relative_path) - MapSet.member?(plan.requested_years, String.to_integer(year)) - - true -> - false - end - end - @spec route_html_path?(String.t()) :: boolean() def route_html_path?(relative_path), do: String.ends_with?(relative_path, "index.html") diff --git a/lib/bds/rendering/post_rendering.ex b/lib/bds/rendering/post_rendering.ex index 6ae3df6..de37ce6 100644 --- a/lib/bds/rendering/post_rendering.ex +++ b/lib/bds/rendering/post_rendering.ex @@ -20,7 +20,7 @@ defmodule BDS.Rendering.PostRendering do metadata = RenderMetadata.project_metadata(project_id) template_context = TemplateSelection.template_render_context(project_id) - language = MapUtils.attr(assigns, :language, metadata.main_language || "en") + language = MapUtils.attr(assigns, :language) || metadata.main_language || "en" main_language = metadata.main_language || language post_record = Map.get(assigns, :_post_record) || load_post_record(assigns) @@ -58,24 +58,26 @@ defmodule BDS.Rendering.PostRendering do %{ language: language, - language_prefix: assign_override( - assigns, - :language_prefix, - "language_prefix", - LinksAndLanguages.language_prefix(language, main_language) - ), + language_prefix: + assign_override( + assigns, + :language_prefix, + "language_prefix", + LinksAndLanguages.language_prefix(language, main_language) + ), page_title: Map.get( assigns, :page_title, MapUtils.attr(assigns, :title) ), - pico_stylesheet_href: assign_override( - assigns, - :pico_stylesheet_href, - "pico_stylesheet_href", - RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme) - ), + pico_stylesheet_href: + assign_override( + assigns, + :pico_stylesheet_href, + "pico_stylesheet_href", + RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme) + ), html_theme_attribute: assign_override(assigns, :html_theme_attribute, "html_theme_attribute", nil), blog_languages: RenderMetadata.blog_languages(metadata, language), diff --git a/priv/gettext/de/LC_MESSAGES/ui.po b/priv/gettext/de/LC_MESSAGES/ui.po index 3b814bd..4c4cb07 100644 --- a/priv/gettext/de/LC_MESSAGES/ui.po +++ b/priv/gettext/de/LC_MESSAGES/ui.po @@ -221,6 +221,86 @@ msgstr "Auswahl anwenden" msgid "Apply Theme" 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/ui/sidebar.ex:331 #, elixir-autogen, elixir-format diff --git a/priv/gettext/en/LC_MESSAGES/ui.po b/priv/gettext/en/LC_MESSAGES/ui.po index 8044215..878cfa3 100644 --- a/priv/gettext/en/LC_MESSAGES/ui.po +++ b/priv/gettext/en/LC_MESSAGES/ui.po @@ -221,6 +221,86 @@ msgstr "" msgid "Apply Theme" 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/ui/sidebar.ex:331 #, elixir-autogen, elixir-format diff --git a/priv/gettext/es/LC_MESSAGES/ui.po b/priv/gettext/es/LC_MESSAGES/ui.po index 1ce16b0..d994bad 100644 --- a/priv/gettext/es/LC_MESSAGES/ui.po +++ b/priv/gettext/es/LC_MESSAGES/ui.po @@ -221,6 +221,86 @@ msgstr "Aplicar seleccionados" msgid "Apply Theme" 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/ui/sidebar.ex:331 #, elixir-autogen, elixir-format diff --git a/priv/gettext/fr/LC_MESSAGES/ui.po b/priv/gettext/fr/LC_MESSAGES/ui.po index 054dc4a..72b909d 100644 --- a/priv/gettext/fr/LC_MESSAGES/ui.po +++ b/priv/gettext/fr/LC_MESSAGES/ui.po @@ -221,6 +221,86 @@ msgstr "Appliquer la selection" msgid "Apply Theme" 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 d’application de la validation du site mises en file" + +#: lib/bds/desktop/shell_commands.ex +msgid "Build Search Index" +msgstr "Construire l’index 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 l’application de la validation..." + +#: lib/bds/desktop/shell_commands.ex +msgid "Prepare Validation Apply" +msgstr "Préparer l’application 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/ui/sidebar.ex:331 #, elixir-autogen, elixir-format diff --git a/priv/gettext/it/LC_MESSAGES/ui.po b/priv/gettext/it/LC_MESSAGES/ui.po index 18ec9fb..5e8dd0c 100644 --- a/priv/gettext/it/LC_MESSAGES/ui.po +++ b/priv/gettext/it/LC_MESSAGES/ui.po @@ -221,6 +221,86 @@ msgstr "Applica selezionati" msgid "Apply Theme" 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/ui/sidebar.ex:331 #, elixir-autogen, elixir-format diff --git a/priv/gettext/ui.pot b/priv/gettext/ui.pot index aaf215d..fc725e3 100644 --- a/priv/gettext/ui.pot +++ b/priv/gettext/ui.pot @@ -11,7 +11,7 @@ msgid "" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:333 +#: lib/bds/desktop/shell_live/misc_editor.ex:319 #, elixir-autogen, elixir-format msgid "%{canonical} = %{translation}" msgstr "" @@ -92,7 +92,7 @@ msgstr "" #: 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/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 #, elixir-autogen, elixir-format msgid "AI Suggestions" @@ -234,6 +234,86 @@ msgstr "" msgid "Apply Theme" 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/ui/sidebar.ex:331 #, elixir-autogen, elixir-format @@ -272,12 +352,12 @@ msgstr "" #: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_live.ex:422 #: 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:353 -#: lib/bds/desktop/shell_live/media_editor.ex:546 +#: lib/bds/desktop/shell_live/media_editor.ex:171 +#: lib/bds/desktop/shell_live/media_editor.ex:364 +#: lib/bds/desktop/shell_live/media_editor.ex:557 #: 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:804 +#: lib/bds/desktop/shell_live/post_editor.ex:763 +#: lib/bds/desktop/shell_live/post_editor.ex:812 #, elixir-autogen, elixir-format msgid "Automatic AI actions stay gated by airplane mode." msgstr "" @@ -380,8 +460,8 @@ msgstr "" #: lib/bds/desktop/shell_live/import_editor.ex:1024 #: lib/bds/desktop/shell_live/import_editor.ex:1188 #: 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:756 +#: lib/bds/desktop/shell_live/misc_editor.ex:741 +#: 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/settings_editor/managed_categories.ex:59 #: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:75 @@ -439,7 +519,7 @@ msgstr "" msgid "Check Syntax" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:486 +#: lib/bds/desktop/shell_live/misc_editor.ex:472 #, elixir-autogen, elixir-format msgid "Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}" msgstr "" @@ -675,7 +755,7 @@ msgstr "" msgid "Delete Media" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:396 +#: lib/bds/desktop/shell_live/media_editor.ex:407 #, elixir-autogen, elixir-format msgid "Delete Translation" msgstr "" @@ -691,7 +771,7 @@ msgstr "" msgid "Delete this unpublished draft" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:120 +#: lib/bds/desktop/shell_live/misc_editor.ex:106 #, elixir-autogen, elixir-format msgid "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk" msgstr "" @@ -716,14 +796,14 @@ msgstr "" msgid "Detect" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:159 -#: 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:170 #: 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/post_editor.ex:754 -#: lib/bds/desktop/shell_live/post_editor.ex:783 -#: lib/bds/desktop/shell_live/post_editor.ex:789 +#: lib/bds/desktop/shell_live/post_editor.ex:762 +#: lib/bds/desktop/shell_live/post_editor.ex:791 +#: lib/bds/desktop/shell_live/post_editor.ex:797 #, elixir-autogen, elixir-format msgid "Detect Language" msgstr "" @@ -854,8 +934,8 @@ msgstr "" msgid "Editor Settings" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:757 -#: lib/bds/desktop/shell_live/misc_editor.ex:780 +#: lib/bds/desktop/shell_live/misc_editor.ex:743 +#: lib/bds/desktop/shell_live/misc_editor.ex:766 #, elixir-autogen, elixir-format msgid "Embeddings" msgstr "" @@ -980,11 +1060,11 @@ msgstr "" msgid "Find Duplicate Posts" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:176 -#: lib/bds/desktop/shell_live/misc_editor.ex:182 -#: lib/bds/desktop/shell_live/misc_editor.ex:209 -#: lib/bds/desktop/shell_live/misc_editor.ex:216 -#: lib/bds/desktop/shell_live/misc_editor.ex:503 +#: lib/bds/desktop/shell_live/misc_editor.ex:162 +#: lib/bds/desktop/shell_live/misc_editor.ex:168 +#: lib/bds/desktop/shell_live/misc_editor.ex:195 +#: lib/bds/desktop/shell_live/misc_editor.ex:202 +#: lib/bds/desktop/shell_live/misc_editor.ex:489 #: lib/bds/ui/registry.ex:139 #, elixir-autogen, elixir-format msgid "Find Duplicates" @@ -1013,7 +1093,7 @@ msgstr "" msgid "Git" 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 #, elixir-autogen, elixir-format msgid "Git Diff" @@ -1048,8 +1128,8 @@ msgstr "" #: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_live/index.html.heex:666 -#: lib/bds/desktop/shell_live/media_editor.ex:711 -#: lib/bds/desktop/shell_live/post_editor.ex:1006 +#: lib/bds/desktop/shell_live/media_editor.ex:731 +#: lib/bds/desktop/shell_live/post_editor.ex:1036 #, elixir-autogen, elixir-format msgid "Idle" msgstr "" @@ -1219,8 +1299,8 @@ msgstr "" msgid "Language" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:210 -#: lib/bds/desktop/shell_live/post_editor.ex:790 +#: lib/bds/desktop/shell_live/media_editor.ex:221 +#: lib/bds/desktop/shell_live/post_editor.ex:798 #, elixir-autogen, elixir-format msgid "Language detection failed." msgstr "" @@ -1230,7 +1310,7 @@ msgstr "" msgid "Light" 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 #, elixir-autogen, elixir-format msgid "Link to Post" @@ -1304,7 +1384,7 @@ msgstr "" msgid "Mapped" 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 #, elixir-autogen, elixir-format msgid "Markdown" @@ -1316,10 +1396,10 @@ msgid "Max Posts Per Page" msgstr "" #: lib/bds/desktop/menu_bar.ex:168 -#: lib/bds/desktop/shell_live/media_editor.ex:498 -#: lib/bds/desktop/shell_live/media_editor.ex:502 -#: lib/bds/desktop/shell_live/misc_editor.ex:749 -#: lib/bds/desktop/shell_live/misc_editor.ex:776 +#: lib/bds/desktop/shell_live/media_editor.ex:509 +#: lib/bds/desktop/shell_live/media_editor.ex:513 +#: lib/bds/desktop/shell_live/misc_editor.ex:735 +#: lib/bds/desktop/shell_live/misc_editor.ex:762 #: lib/bds/desktop/shell_live/sidebar_components.ex:654 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/ui/registry.ex:30 @@ -1334,7 +1414,7 @@ msgstr "" msgid "Media (%{count})" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:498 +#: lib/bds/desktop/shell_live/media_editor.ex:509 #, elixir-autogen, elixir-format msgid "Media saved" msgstr "" @@ -1361,9 +1441,9 @@ msgid "Metadata" msgstr "" #: lib/bds/desktop/menu_bar.ex:185 -#: lib/bds/desktop/shell_live/misc_editor.ex:228 -#: lib/bds/desktop/shell_live/misc_editor.ex:240 -#: lib/bds/desktop/shell_live/misc_editor.ex:462 +#: lib/bds/desktop/shell_live/misc_editor.ex:214 +#: lib/bds/desktop/shell_live/misc_editor.ex:226 +#: lib/bds/desktop/shell_live/misc_editor.ex:448 #: lib/bds/ui/registry.ex:111 #, elixir-autogen, elixir-format msgid "Metadata Diff" @@ -1465,7 +1545,7 @@ msgstr "" msgid "No git history yet" 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/post_editor_html/post_editor.html.heex:321 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:333 @@ -1505,12 +1585,12 @@ msgstr "" msgid "No media files" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:592 +#: lib/bds/desktop/shell_live/misc_editor.ex:578 #, elixir-autogen, elixir-format msgid "No metadata diff items selected" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:621 +#: lib/bds/desktop/shell_live/misc_editor.ex:607 #, elixir-autogen, elixir-format msgid "No orphan files selected" msgstr "" @@ -1535,7 +1615,7 @@ msgstr "" msgid "No posts yet" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:589 +#: lib/bds/desktop/shell_live/misc_editor.ex:575 #, elixir-autogen, elixir-format msgid "No repair action available" msgstr "" @@ -1560,7 +1640,7 @@ msgstr "" msgid "No translations" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:567 +#: lib/bds/desktop/shell_live/misc_editor.ex:553 #, elixir-autogen, elixir-format msgid "No unstaged changes" msgstr "" @@ -1775,7 +1855,7 @@ msgstr "" msgid "Pages (%{count})" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:176 +#: lib/bds/desktop/shell_live/misc_editor.ex:162 #, elixir-autogen, elixir-format msgid "Pair dismissed" msgstr "" @@ -1801,17 +1881,17 @@ msgstr "" msgid "Persist the detected language for this media item" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:747 -#: lib/bds/desktop/shell_live/post_editor.ex:529 -#: lib/bds/desktop/shell_live/post_editor.ex:533 -#: lib/bds/desktop/shell_live/post_editor.ex:572 -#: lib/bds/desktop/shell_live/post_editor.ex:576 -#: lib/bds/desktop/shell_live/post_editor.ex:614 -#: lib/bds/desktop/shell_live/post_editor.ex:629 -#: lib/bds/desktop/shell_live/post_editor.ex:658 -#: lib/bds/desktop/shell_live/post_editor.ex:661 -#: lib/bds/desktop/shell_live/post_editor.ex:741 -#: lib/bds/desktop/shell_live/post_editor.ex:744 +#: lib/bds/desktop/shell_live/misc_editor.ex:733 +#: lib/bds/desktop/shell_live/post_editor.ex:537 +#: lib/bds/desktop/shell_live/post_editor.ex:541 +#: lib/bds/desktop/shell_live/post_editor.ex:580 +#: lib/bds/desktop/shell_live/post_editor.ex:584 +#: lib/bds/desktop/shell_live/post_editor.ex:622 +#: lib/bds/desktop/shell_live/post_editor.ex:637 +#: lib/bds/desktop/shell_live/post_editor.ex:666 +#: lib/bds/desktop/shell_live/post_editor.ex:669 +#: lib/bds/desktop/shell_live/post_editor.ex:749 +#: lib/bds/desktop/shell_live/post_editor.ex:752 #: lib/bds/desktop/shell_live/sidebar_components.ex:651 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/ui/registry.ex:99 @@ -1836,24 +1916,24 @@ msgstr "" msgid "Post Template" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:315 +#: lib/bds/desktop/shell_live/misc_editor.ex:301 #, elixir-autogen, elixir-format msgid "Post is marked as do-not-translate but has translations" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:572 +#: lib/bds/desktop/shell_live/post_editor.ex:580 #, elixir-autogen, elixir-format msgid "Post published" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:529 +#: lib/bds/desktop/shell_live/post_editor.ex:537 #, elixir-autogen, elixir-format msgid "Post saved" msgstr "" #: lib/bds/desktop/menu_bar.ex:167 -#: lib/bds/desktop/shell_live/misc_editor.ex:678 -#: lib/bds/desktop/shell_live/misc_editor.ex:775 +#: lib/bds/desktop/shell_live/misc_editor.ex:664 +#: lib/bds/desktop/shell_live/misc_editor.ex:761 #: lib/bds/ui/registry.ex:14 #: lib/bds/ui/sidebar.ex:271 #, elixir-autogen, elixir-format @@ -1870,7 +1950,7 @@ msgstr "" msgid "Preferences" 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 #, elixir-autogen, elixir-format msgid "Preview" @@ -1892,8 +1972,8 @@ msgid "Preview unavailable" msgstr "" #: 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:779 +#: lib/bds/desktop/shell_live/misc_editor.ex:739 +#: lib/bds/desktop/shell_live/misc_editor.ex:765 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:27 #: lib/bds/ui/sidebar.ex:762 #, elixir-autogen, elixir-format @@ -1939,19 +2019,19 @@ msgid "Publish Selected" msgstr "" #: 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/ui/sidebar.ex:324 #, elixir-autogen, elixir-format msgid "Published" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:318 +#: lib/bds/desktop/shell_live/misc_editor.ex:304 #, elixir-autogen, elixir-format msgid "Published translation has content stuck in DB instead of filesystem" 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:57 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:338 @@ -2038,8 +2118,8 @@ msgstr "" msgid "Refresh Online Models" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:368 -#: lib/bds/desktop/shell_live/media_editor.ex:377 +#: lib/bds/desktop/shell_live/media_editor.ex:379 +#: lib/bds/desktop/shell_live/media_editor.ex:388 #, elixir-autogen, elixir-format msgid "Refresh Translation" msgstr "" @@ -2095,14 +2175,14 @@ msgstr "" msgid "Replace" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:142 -#: lib/bds/desktop/shell_live/media_editor.ex:150 +#: lib/bds/desktop/shell_live/media_editor.ex:153 +#: lib/bds/desktop/shell_live/media_editor.ex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:86 #, elixir-autogen, elixir-format msgid "Replace File" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:116 +#: lib/bds/desktop/shell_live/media_editor.ex:127 #, elixir-autogen, elixir-format msgid "Replace Media File" msgstr "" @@ -2137,7 +2217,7 @@ msgstr "" msgid "Result" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:1005 +#: lib/bds/desktop/shell_live/post_editor.ex:1035 #, elixir-autogen, elixir-format msgid "Reverted" msgstr "" @@ -2183,13 +2263,13 @@ msgstr "" msgid "Save" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:328 +#: lib/bds/desktop/shell_live/media_editor.ex:339 #, elixir-autogen, elixir-format msgid "Save Translation" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:710 -#: lib/bds/desktop/shell_live/post_editor.ex:1003 +#: lib/bds/desktop/shell_live/media_editor.ex:730 +#: lib/bds/desktop/shell_live/post_editor.ex:1033 #, elixir-autogen, elixir-format msgid "Saved" msgstr "" @@ -2199,7 +2279,7 @@ msgstr "" msgid "Scanning entries..." 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_delete.ex:176 #: 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." 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:124 #: 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" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:210 +#: lib/bds/desktop/shell_live/misc_editor.ex:196 #, elixir-autogen, elixir-format msgid "Selected pairs dismissed" msgstr "" @@ -2366,9 +2446,7 @@ msgstr "" msgid "Site" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:95 -#: lib/bds/desktop/shell_live/misc_editor.ex:105 -#: lib/bds/desktop/shell_live/misc_editor.ex:426 +#: lib/bds/desktop/shell_live/misc_editor.ex:412 #: lib/bds/ui/registry.ex:125 #, elixir-autogen, elixir-format msgid "Site Validation" @@ -2510,7 +2588,7 @@ msgstr "" msgid "Technology" 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/sidebar_components.ex:660 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179 @@ -2534,7 +2612,7 @@ msgstr "" msgid "Template syntax is valid" 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:116 #: lib/bds/desktop/shell_live/template_editor.ex:121 @@ -2658,39 +2736,35 @@ msgstr "" msgid "Toggle sidebar" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:352 -#: lib/bds/desktop/shell_live/media_editor.ex:545 -#: 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.ex:363 +#: lib/bds/desktop/shell_live/media_editor.ex:556 #: 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:832 -#: lib/bds/desktop/shell_live/post_editor.ex:837 +#: lib/bds/desktop/shell_live/post_editor.ex:811 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #, elixir-autogen, elixir-format msgid "Translate" 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:133 -#: lib/bds/desktop/shell_live/misc_editor.ex:482 +#: lib/bds/desktop/shell_live/misc_editor.ex:468 #, elixir-autogen, elixir-format msgid "Translation Validation" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:312 +#: lib/bds/desktop/shell_live/misc_editor.ex:298 #, elixir-autogen, elixir-format msgid "Translation language matches canonical post language" msgstr "" -#: lib/bds/desktop/shell_live/misc_editor.ex:321 +#: lib/bds/desktop/shell_live/misc_editor.ex:307 #, elixir-autogen, elixir-format msgid "Translation points to a missing source post" msgstr "" #: 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:750 +#: lib/bds/desktop/shell_live/misc_editor.ex:734 +#: lib/bds/desktop/shell_live/misc_editor.ex:736 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:129 #: lib/bds/ui/registry.ex:131 #, elixir-autogen, elixir-format @@ -2735,14 +2809,14 @@ msgstr "" msgid "Unknown" msgstr "" -#: lib/bds/desktop/shell_live/media_editor.ex:269 +#: lib/bds/desktop/shell_live/media_editor.ex:280 #, elixir-autogen, elixir-format msgid "Unlink from Post" 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/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 #, elixir-autogen, elixir-format msgid "Unsaved" @@ -2811,11 +2885,6 @@ msgstr "" msgid "Validate Translations" 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 #, elixir-autogen, elixir-format msgid "View" @@ -3243,12 +3312,12 @@ msgstr "" msgid "Move this post to the archive" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:658 +#: lib/bds/desktop/shell_live/post_editor.ex:666 #, elixir-autogen, elixir-format msgid "Post archived" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:741 +#: lib/bds/desktop/shell_live/post_editor.ex:749 #, elixir-autogen, elixir-format msgid "Post unarchived" msgstr "" @@ -3427,18 +3496,18 @@ msgstr "" msgid "Open a project before importing a blogmark." msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:684 +#: lib/bds/desktop/shell_live/post_editor.ex:692 #, elixir-autogen, elixir-format msgid "Added %{name}" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:691 +#: lib/bds/desktop/shell_live/post_editor.ex:699 #, elixir-autogen, elixir-format msgid "Failed to import %{path}: %{reason}" msgstr "" -#: lib/bds/desktop/shell_live/post_editor.ex:683 -#: lib/bds/desktop/shell_live/post_editor.ex:690 +#: lib/bds/desktop/shell_live/post_editor.ex:691 +#: lib/bds/desktop/shell_live/post_editor.ex:698 #, elixir-autogen, elixir-format msgid "Insert Image" msgstr "" diff --git a/specs/editor_misc.allium b/specs/editor_misc.allium index f19dda3..2aaab81 100644 --- a/specs/editor_misc.allium +++ b/specs/editor_misc.allium @@ -458,10 +458,11 @@ rule SiteValidationScan { rule SiteValidationApply { when: SiteValidationApplyRequested(report) -- 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 - -- Regenerates calendar if anything changed - -- Rebuilds search index if anything rendered or deleted + -- Regenerates calendar if anything changed (feed/atom/404/sitemap/search + -- index are NOT rebuilt on apply) ensures: ApplyValidationRequested(report.project_id, report.affected_sections) } diff --git a/test/bds/desktop/shell_commands_test.exs b/test/bds/desktop/shell_commands_test.exs index c855263..12f0411 100644 --- a/test/bds/desktop/shell_commands_test.exs +++ b/test/bds/desktop/shell_commands_test.exs @@ -368,6 +368,76 @@ defmodule BDS.Desktop.ShellCommandsTest do assert Map.has_key?(completed.result.payload.summary, :updated_count) 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, "obsolete") + + 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 assert {:ok, result} = ShellCommands.execute("metadata_diff") diff --git a/test/bds/generation_test.exs b/test/bds/generation_test.exs index d797636..c1b86b6 100644 --- a/test/bds/generation_test.exs +++ b/test/bds/generation_test.exs @@ -914,6 +914,321 @@ defmodule BDS.GenerationTest do assert clean_report.updated_post_url_paths == [] 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", %{ project: project, temp_dir: temp_dir @@ -1343,6 +1658,23 @@ defmodule BDS.GenerationTest do # Pushes generation timestamps and html mtimes far into the past so a # subsequent source-file write is newer by more than the second-granularity # 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 past_posix = System.os_time(:second) - 120