feat: more implementations of partial code and cleanup
This commit is contained in:
@@ -258,7 +258,6 @@ defmodule BDS.Embeddings do
|
||||
{:ok, suggestions}
|
||||
else
|
||||
{:error, :not_found} -> {:ok, []}
|
||||
{:disabled, _project_id} -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -411,7 +410,6 @@ defmodule BDS.Embeddings do
|
||||
defp enabled_for_project?(project_id) do
|
||||
case Metadata.get_project_metadata(project_id) do
|
||||
{:ok, metadata} -> metadata.semantic_similarity_enabled == true
|
||||
_other -> false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -453,7 +451,10 @@ defmodule BDS.Embeddings do
|
||||
end
|
||||
end
|
||||
|
||||
defp compose_embedding_source(title, content), do: "#{title || ""}\n\n#{content || ""}"
|
||||
defp compose_embedding_source(title, content), do: string_or_empty(title) <> "\n\n" <> string_or_empty(content)
|
||||
|
||||
defp string_or_empty(nil), do: ""
|
||||
defp string_or_empty(value) when is_binary(value), do: value
|
||||
|
||||
defp post_content_hash(%Post{} = post) do
|
||||
body = resolve_post_body(post)
|
||||
|
||||
@@ -50,6 +50,102 @@ defmodule BDS.Generation do
|
||||
end
|
||||
end
|
||||
|
||||
def validate_site(project_id, sections \\ @core_sections)
|
||||
|
||||
def validate_site(project_id, sections) when is_binary(project_id) and is_list(sections) do
|
||||
with {:ok, plan} <- plan_generation(project_id, sections) do
|
||||
expected_outputs = build_outputs(plan)
|
||||
expected_paths = MapSet.new(Enum.map(expected_outputs, &elem(&1, 0)))
|
||||
expected_hashes = Map.new(expected_outputs, fn {relative_path, content} -> {relative_path, sha256(content)} end)
|
||||
actual_files = disk_generated_files(project_id)
|
||||
actual_paths = MapSet.new(Map.keys(actual_files))
|
||||
|
||||
missing_pages =
|
||||
expected_paths
|
||||
|> MapSet.difference(actual_paths)
|
||||
|> MapSet.to_list()
|
||||
|> Enum.sort()
|
||||
|
||||
extra_pages =
|
||||
actual_paths
|
||||
|> MapSet.difference(expected_paths)
|
||||
|> MapSet.to_list()
|
||||
|> Enum.sort()
|
||||
|
||||
stale_pages =
|
||||
expected_hashes
|
||||
|> Enum.filter(fn {relative_path, expected_hash} ->
|
||||
case actual_files do
|
||||
%{^relative_path => actual_hash} -> actual_hash != expected_hash
|
||||
_other -> false
|
||||
end
|
||||
end)
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|> Enum.sort()
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
missing_pages: missing_pages,
|
||||
extra_pages: extra_pages,
|
||||
stale_pages: stale_pages,
|
||||
sections: affected_sections(missing_pages ++ extra_pages ++ stale_pages)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
def apply_validation(project_id, sections) when is_binary(project_id) and is_list(sections) do
|
||||
with {:ok, plan} <- plan_generation(project_id, sections) do
|
||||
expected_outputs = build_outputs(plan)
|
||||
expected_paths = MapSet.new(Enum.map(expected_outputs, &elem(&1, 0)))
|
||||
actual_files = disk_generated_files(project_id)
|
||||
project = Projects.get_project!(project_id)
|
||||
now = Persistence.now_ms()
|
||||
|
||||
Enum.each(expected_outputs, fn {relative_path, content} ->
|
||||
expected_hash = sha256(content)
|
||||
|
||||
case actual_files do
|
||||
%{^relative_path => ^expected_hash} ->
|
||||
:ok
|
||||
|
||||
_other ->
|
||||
:ok = Persistence.atomic_write(output_path(project, relative_path), content)
|
||||
|
||||
%GeneratedFileHash{}
|
||||
|> GeneratedFileHash.changeset(%{
|
||||
project_id: project_id,
|
||||
relative_path: relative_path,
|
||||
content_hash: expected_hash,
|
||||
updated_at: now
|
||||
})
|
||||
|> Repo.insert!(
|
||||
on_conflict: [set: [content_hash: expected_hash, updated_at: now]],
|
||||
conflict_target: [:project_id, :relative_path]
|
||||
)
|
||||
end
|
||||
end)
|
||||
|
||||
disk_generated_files(project_id)
|
||||
|> Map.keys()
|
||||
|> Enum.filter(fn relative_path ->
|
||||
path_section(relative_path) in plan.sections and not MapSet.member?(expected_paths, relative_path)
|
||||
end)
|
||||
|> Enum.each(fn relative_path ->
|
||||
_ = File.rm(output_path(project, relative_path))
|
||||
|
||||
Repo.delete_all(
|
||||
from generated_file in GeneratedFileHash,
|
||||
where:
|
||||
generated_file.project_id == ^project_id and
|
||||
generated_file.relative_path == ^relative_path
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, generated_files} = list_generated_files(project_id)
|
||||
{:ok, %{sections: plan.sections, generated_files: generated_files}}
|
||||
end
|
||||
end
|
||||
|
||||
def post_output_path(%Post{} = post), do: post_output_path(post, nil)
|
||||
|
||||
def post_output_path(%Post{} = post, language) do
|
||||
@@ -166,6 +262,64 @@ defmodule BDS.Generation do
|
||||
core_outputs ++ single_outputs ++ archive_outputs ++ sitemap
|
||||
end
|
||||
|
||||
defp disk_generated_files(project_id) do
|
||||
project = Projects.get_project!(project_id)
|
||||
html_root = output_path(project, "")
|
||||
|
||||
case File.ls(html_root) do
|
||||
{:ok, _entries} ->
|
||||
html_root
|
||||
|> Path.join("**/*")
|
||||
|> Path.wildcard(match_dot: false)
|
||||
|> Enum.filter(&File.regular?/1)
|
||||
|> Enum.map(fn path ->
|
||||
relative_path = Path.relative_to(path, html_root)
|
||||
|
||||
{relative_path,
|
||||
path
|
||||
|> File.read!()
|
||||
|> sha256()}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
{:error, :enoent} ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp affected_sections(paths) do
|
||||
paths
|
||||
|> Enum.map(&path_section/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.uniq()
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp path_section(relative_path) do
|
||||
segments = String.split(relative_path, "/", trim: true)
|
||||
|
||||
case strip_language_prefix(segments) do
|
||||
["404.html"] -> :core
|
||||
["index.html"] -> :core
|
||||
["sitemap.xml"] -> :core
|
||||
["feed.xml"] -> :core
|
||||
["atom.xml"] -> :core
|
||||
["calendar.json"] -> :core
|
||||
["pagefind" | _rest] -> :core
|
||||
[year, month, day, _slug, "index.html"] when byte_size(year) == 4 and byte_size(month) == 2 and byte_size(day) == 2 -> :single
|
||||
["category" | _rest] -> :category
|
||||
["tag" | _rest] -> :tag
|
||||
[year, "index.html"] when byte_size(year) == 4 -> :date
|
||||
[year, month, "index.html"] when byte_size(year) == 4 and byte_size(month) == 2 -> :date
|
||||
_other -> :core
|
||||
end
|
||||
end
|
||||
|
||||
defp strip_language_prefix([language | rest]) when language in ["en", "de", "fr", "it", "es"],
|
||||
do: rest
|
||||
|
||||
defp strip_language_prefix(segments), do: segments
|
||||
|
||||
defp build_archive_outputs(plan, published_posts) do
|
||||
languages = plan.blog_languages
|
||||
|
||||
@@ -692,17 +846,6 @@ defmodule BDS.Generation do
|
||||
end
|
||||
end
|
||||
|
||||
defp render_list_output(
|
||||
_plan,
|
||||
_language,
|
||||
_page_title,
|
||||
_posts,
|
||||
_archive_context,
|
||||
_pagination,
|
||||
fallback
|
||||
),
|
||||
do: fallback.()
|
||||
|
||||
defp render_not_found_output(%{project_id: project_id, language: main_language}, language)
|
||||
when is_binary(project_id) do
|
||||
case Rendering.render_not_found_page(project_id, %{
|
||||
@@ -714,8 +857,6 @@ defmodule BDS.Generation do
|
||||
end
|
||||
end
|
||||
|
||||
defp render_not_found_output(_plan, language), do: render_not_found_page(language)
|
||||
|
||||
defp language_prefix(language, main_language) when language == main_language, do: ""
|
||||
defp language_prefix(nil, _main_language), do: ""
|
||||
defp language_prefix(language, _main_language), do: "/#{language}"
|
||||
|
||||
@@ -76,10 +76,6 @@ defmodule BDS.Persistence do
|
||||
{:error, _reason} = error ->
|
||||
_ = File.rm(temp_path)
|
||||
error
|
||||
|
||||
error ->
|
||||
_ = File.rm(temp_path)
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -409,7 +409,7 @@ defmodule BDS.Posts do
|
||||
end
|
||||
|
||||
defp unique_slug(project_id, base_slug) do
|
||||
normalized = if base_slug in [nil, ""], do: "untitled", else: base_slug
|
||||
normalized = if base_slug == "", do: "untitled", else: base_slug
|
||||
|
||||
if slug_available?(project_id, normalized) do
|
||||
normalized
|
||||
@@ -486,7 +486,7 @@ defmodule BDS.Posts do
|
||||
{:tags, post.tags || []},
|
||||
{:categories, post.categories || []}
|
||||
],
|
||||
post.content || ""
|
||||
post.content
|
||||
)
|
||||
end
|
||||
|
||||
@@ -671,7 +671,7 @@ defmodule BDS.Posts do
|
||||
{:updated_at, translation.updated_at},
|
||||
{:published_at, published_at}
|
||||
],
|
||||
translation.content || ""
|
||||
translation.content
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -28,17 +28,19 @@ defmodule BDS.Preview do
|
||||
end
|
||||
|
||||
def request(project_id, request_path) when is_binary(project_id) and is_binary(request_path) do
|
||||
GenServer.call(__MODULE__, {:request, project_id, request_path})
|
||||
{path, query_params} = split_request_path(request_path)
|
||||
GenServer.call(__MODULE__, {:request, project_id, path, query_params})
|
||||
end
|
||||
|
||||
def preview_draft(project_id, request_path, post_id)
|
||||
when is_binary(project_id) and is_binary(request_path) and is_binary(post_id) do
|
||||
post = Posts.get_post!(post_id)
|
||||
{_path, query_params} = split_request_path(request_path)
|
||||
|
||||
GenServer.call(__MODULE__, {
|
||||
:preview_draft,
|
||||
project_id,
|
||||
request_path,
|
||||
query_params,
|
||||
%{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
@@ -97,22 +99,23 @@ defmodule BDS.Preview do
|
||||
{:reply, :ok, next_state}
|
||||
end
|
||||
|
||||
def handle_call({:request, project_id, request_path}, _from, state) do
|
||||
def handle_call({:request, project_id, request_path, query_params}, _from, state) do
|
||||
with :ok <- ensure_running(state.current, project_id),
|
||||
{:ok, response} <- resolve_request(state.current, request_path) do
|
||||
{:ok, response} <- resolve_request(state.current, request_path, query_params) do
|
||||
{:reply, {:ok, response}, state}
|
||||
else
|
||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:preview_draft, project_id, _request_path, post}, _from, state) do
|
||||
def handle_call({:preview_draft, project_id, query_params, post}, _from, state) do
|
||||
with :ok <- ensure_running(state.current, project_id) do
|
||||
body =
|
||||
case Rendering.render_post_page(project_id, Map.get(post, :template_slug), post) do
|
||||
{:ok, rendered} -> rendered
|
||||
{:error, _reason} -> render_draft(post)
|
||||
end
|
||||
|> apply_preview_overrides(query_params)
|
||||
|
||||
response = %{
|
||||
content_type: "text/html",
|
||||
@@ -132,13 +135,13 @@ defmodule BDS.Preview do
|
||||
case query_params["post_id"] do
|
||||
post_id when is_binary(post_id) ->
|
||||
if String.starts_with?(request_path, "/draft/") do
|
||||
resolve_draft_request(project_id, post_id)
|
||||
resolve_draft_request(project_id, post_id, query_params)
|
||||
else
|
||||
resolve_request(state.current, request_path)
|
||||
resolve_request(state.current, request_path, query_params)
|
||||
end
|
||||
|
||||
_other ->
|
||||
resolve_request(state.current, request_path)
|
||||
resolve_request(state.current, request_path, query_params)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -148,7 +151,7 @@ defmodule BDS.Preview do
|
||||
defp ensure_running(%{project_id: project_id, is_running: true}, project_id), do: :ok
|
||||
defp ensure_running(_server, _project_id), do: {:error, :not_running}
|
||||
|
||||
defp resolve_request(server, request_path) do
|
||||
defp resolve_request(server, request_path, query_params) do
|
||||
with {:ok, relative_path, kind} <- route_request(request_path) do
|
||||
full_path =
|
||||
case kind do
|
||||
@@ -162,14 +165,15 @@ defmodule BDS.Preview do
|
||||
|
||||
resolved_path ->
|
||||
case read_response(resolved_path) do
|
||||
{:error, :not_found} -> render_not_found_response(server.project_id)
|
||||
{:error, :not_found} -> render_not_found_response(server.project_id, query_params)
|
||||
{:ok, response} -> {:ok, apply_response_overrides(response, query_params)}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_draft_request(project_id, post_id) do
|
||||
defp resolve_draft_request(project_id, post_id, query_params) do
|
||||
try do
|
||||
post = Posts.get_post!(post_id)
|
||||
|
||||
@@ -190,6 +194,7 @@ defmodule BDS.Preview do
|
||||
{:ok, rendered} -> rendered
|
||||
{:error, _reason} -> render_draft(payload)
|
||||
end
|
||||
|> apply_preview_overrides(query_params)
|
||||
|
||||
{:ok, %{content_type: "text/html", body: body}}
|
||||
else
|
||||
@@ -378,16 +383,70 @@ defmodule BDS.Preview do
|
||||
end
|
||||
end
|
||||
|
||||
defp render_not_found_response(project_id) do
|
||||
defp render_not_found_response(project_id, query_params) do
|
||||
body =
|
||||
case Rendering.render_not_found_page(project_id, %{}) do
|
||||
case Rendering.render_not_found_page(project_id, not_found_assigns(query_params)) do
|
||||
{:ok, rendered} -> rendered
|
||||
{:error, _reason} -> "Not Found"
|
||||
end
|
||||
|> apply_preview_overrides(query_params)
|
||||
|
||||
{:ok, %{status: 404, content_type: "text/html", body: body}}
|
||||
end
|
||||
|
||||
defp split_request_path(request_path) do
|
||||
uri = URI.parse(request_path)
|
||||
{uri.path || "/", URI.decode_query(uri.query || "")}
|
||||
end
|
||||
|
||||
defp apply_response_overrides(%{content_type: content_type, body: body} = response, query_params)
|
||||
when is_binary(content_type) and is_binary(body) do
|
||||
if String.starts_with?(content_type, "text/html") do
|
||||
%{response | body: apply_preview_overrides(body, query_params)}
|
||||
else
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_preview_overrides(body, query_params) when is_binary(body) and is_map(query_params) do
|
||||
body
|
||||
|> override_html_attribute("data-theme", normalize_override(query_params["theme"]))
|
||||
|> override_html_attribute("data-mode", normalize_override(query_params["mode"]))
|
||||
end
|
||||
|
||||
defp normalize_override(nil), do: nil
|
||||
defp normalize_override(""), do: nil
|
||||
defp normalize_override(value), do: String.trim(value)
|
||||
|
||||
defp override_html_attribute(body, _attribute, nil), do: body
|
||||
|
||||
defp override_html_attribute(body, attribute, value) do
|
||||
case Regex.run(~r/<html\b[^>]*>/, body) do
|
||||
[html_tag] ->
|
||||
replacement =
|
||||
if String.contains?(html_tag, attribute <> "=") do
|
||||
Regex.replace(~r/\s#{attribute}="[^"]*"/, html_tag, ~s( #{attribute}="#{value}"), global: false)
|
||||
else
|
||||
String.replace_suffix(html_tag, ">", ~s( #{attribute}="#{value}">))
|
||||
end
|
||||
|
||||
String.replace(body, html_tag, replacement, global: false)
|
||||
|
||||
_other ->
|
||||
body
|
||||
end
|
||||
end
|
||||
|
||||
defp not_found_assigns(query_params) do
|
||||
%{}
|
||||
|> maybe_put_assign("html_theme_attribute", query_params["theme"], fn value -> ~s(data-theme="#{value}") end)
|
||||
|> maybe_put_assign("html_mode_attribute", query_params["mode"], fn value -> ~s(data-mode="#{value}") end)
|
||||
end
|
||||
|
||||
defp maybe_put_assign(assigns, _key, nil, _mapper), do: assigns
|
||||
defp maybe_put_assign(assigns, _key, "", _mapper), do: assigns
|
||||
defp maybe_put_assign(assigns, key, value, mapper), do: Map.put(assigns, key, mapper.(value))
|
||||
|
||||
defp content_type(path) do
|
||||
case Path.extname(path) do
|
||||
".html" -> "text/html"
|
||||
|
||||
@@ -3,7 +3,6 @@ defmodule BDS.Projects do
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Multi
|
||||
alias BDS.Persistence
|
||||
alias BDS.Projects.Project
|
||||
alias BDS.Repo
|
||||
@@ -98,18 +97,19 @@ defmodule BDS.Projects do
|
||||
project ->
|
||||
now = Persistence.now_ms()
|
||||
|
||||
Multi.new()
|
||||
|> Multi.update_all(:clear_previous, from(p in Project, where: p.is_active == true),
|
||||
set: [is_active: false, updated_at: now]
|
||||
)
|
||||
|> Multi.update(
|
||||
:activate,
|
||||
Project.changeset(project, %{is_active: true, updated_at: now})
|
||||
)
|
||||
|> Repo.transaction()
|
||||
Repo.transaction(fn ->
|
||||
Repo.update_all(
|
||||
from(p in Project, where: p.is_active == true),
|
||||
set: [is_active: false, updated_at: now]
|
||||
)
|
||||
|
||||
project
|
||||
|> Project.changeset(%{is_active: true, updated_at: now})
|
||||
|> Repo.update!()
|
||||
end)
|
||||
|> case do
|
||||
{:ok, %{activate: active_project}} -> {:ok, active_project}
|
||||
{:error, _step, reason, _changes} -> {:error, reason}
|
||||
{:ok, active_project} -> {:ok, active_project}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -268,17 +268,13 @@ defmodule BDS.Rendering do
|
||||
end
|
||||
|
||||
defp project_metadata(project_id) do
|
||||
case Metadata.get_project_metadata(project_id) do
|
||||
{:ok, metadata} -> metadata
|
||||
_other -> %{main_language: "en", blog_languages: [], pico_theme: nil}
|
||||
end
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
||||
metadata
|
||||
end
|
||||
|
||||
defp menu_items(project_id) do
|
||||
case Menu.get_menu(project_id) do
|
||||
{:ok, %{items: items}} -> Enum.map(items, &to_template_menu_item/1)
|
||||
_other -> []
|
||||
end
|
||||
{:ok, %{items: items}} = Menu.get_menu(project_id)
|
||||
Enum.map(items, &to_template_menu_item/1)
|
||||
end
|
||||
|
||||
defp to_template_menu_item(item) do
|
||||
|
||||
@@ -206,7 +206,7 @@ defmodule BDS.Rendering.Filters do
|
||||
|
||||
defp split_path_suffix(value) do
|
||||
case Regex.run(~r/^([^?#]*)([?#].*)?$/, String.trim(value)) do
|
||||
[_, path_part, suffix] -> {path_part, suffix || ""}
|
||||
[_, path_part, suffix] -> {path_part, suffix}
|
||||
[_, path_part] -> {path_part, ""}
|
||||
_other -> {value, ""}
|
||||
end
|
||||
|
||||
@@ -8,6 +8,9 @@ defmodule BDS.Scripting.Lua do
|
||||
|
||||
@behaviour BDS.Scripting.Runtime
|
||||
|
||||
@type lua_state :: tuple()
|
||||
@type runtime_result :: {:ok, term(), lua_state} | {:error, term()}
|
||||
|
||||
@impl true
|
||||
def validate(source) when is_binary(source) do
|
||||
case :luerl.load(source, :luerl_sandbox.init()) do
|
||||
@@ -16,9 +19,6 @@ defmodule BDS.Scripting.Lua do
|
||||
|
||||
{:error, errors, warnings} ->
|
||||
{:error, {:compile_error, %{errors: errors, warnings: warnings}}}
|
||||
|
||||
{:lua_error, error, _state} ->
|
||||
{:error, {:lua_error, error}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,6 +102,7 @@ defmodule BDS.Scripting.Lua do
|
||||
end
|
||||
end
|
||||
|
||||
@spec run_entrypoint(binary(), binary(), lua_state, keyword()) :: runtime_result
|
||||
defp run_entrypoint(source, entrypoint, state, opts) do
|
||||
script =
|
||||
IO.iodata_to_binary([
|
||||
@@ -110,27 +111,51 @@ defmodule BDS.Scripting.Lua do
|
||||
entrypoint,
|
||||
"(table.unpack(__bds_args__))\n"
|
||||
])
|
||||
|> String.to_charlist()
|
||||
|
||||
case :luerl_sandbox.run(script, sandbox_flags(opts), state) do
|
||||
{:ok, result, next_state} -> {:ok, result, next_state}
|
||||
{:lua_error, error, _state} -> {:error, {:lua_error, error}}
|
||||
script
|
||||
|> then(&sandbox_run(&1, sandbox_flags(opts), state))
|
||||
|> normalize_sandbox_result()
|
||||
end
|
||||
|
||||
@spec sandbox_run(term(), term(), lua_state) :: term()
|
||||
defp sandbox_run(script, flags, state), do: apply(:luerl_sandbox, :run, [script, flags, state])
|
||||
|
||||
defp normalize_sandbox_result({:ok, result, next_state}), do: {:ok, result, next_state}
|
||||
defp normalize_sandbox_result({:error, {:reductions, count}}), do: {:error, {:reductions_exceeded, count}}
|
||||
defp normalize_sandbox_result({:error, :timeout}), do: {:error, :timeout}
|
||||
defp normalize_sandbox_result({:error, reason}), do: {:error, reason}
|
||||
|
||||
defp normalize_sandbox_result({reply, next_state}) when is_tuple(next_state) do
|
||||
case reply do
|
||||
{:ok, result} -> {:ok, result, next_state}
|
||||
{:ok, result, _reply_state} -> {:ok, result, next_state}
|
||||
{:error, {:reductions, count}} -> {:error, {:reductions_exceeded, count}}
|
||||
{:error, :timeout} -> {:error, :timeout}
|
||||
{:error, reason} -> {:error, reason}
|
||||
other -> {:error, {:unexpected_result, {other, next_state}}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_sandbox_result(other), do: {:error, {:unexpected_result, other}}
|
||||
|
||||
defp sandbox_flags(opts) do
|
||||
config = Application.fetch_env!(:bds, :scripting)
|
||||
|
||||
%{
|
||||
[
|
||||
max_time: Keyword.get(opts, :timeout, Keyword.fetch!(config, :timeout)),
|
||||
max_reductions: Keyword.get(opts, :max_reductions, Keyword.fetch!(config, :max_reductions)),
|
||||
spawn_opts: Keyword.get(opts, :spawn_opts, [])
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp unwrap_result(values) when is_list(values) do
|
||||
case values do
|
||||
[] -> nil
|
||||
[value] -> value
|
||||
_other -> values
|
||||
end
|
||||
end
|
||||
|
||||
defp unwrap_result([]), do: nil
|
||||
defp unwrap_result([value]), do: value
|
||||
defp unwrap_result(values), do: values
|
||||
end
|
||||
|
||||
@@ -132,7 +132,7 @@ defmodule BDS.Scripts do
|
||||
defp default_entrypoint(_kind), do: "main"
|
||||
|
||||
defp unique_slug(project_id, base_slug, fallback, exclude_id \\ nil) do
|
||||
normalized = if base_slug in [nil, ""], do: fallback, else: base_slug
|
||||
normalized = if base_slug == "", do: fallback, else: base_slug
|
||||
|
||||
if slug_available?(project_id, normalized, exclude_id) do
|
||||
normalized
|
||||
|
||||
@@ -173,7 +173,7 @@ defmodule BDS.Templates do
|
||||
end
|
||||
|
||||
defp unique_slug(project_id, base_slug, fallback, exclude_id \\ nil) do
|
||||
normalized = if base_slug in [nil, ""], do: fallback, else: base_slug
|
||||
normalized = if base_slug == "", do: fallback, else: base_slug
|
||||
|
||||
if slug_available?(project_id, normalized, exclude_id) do
|
||||
normalized
|
||||
|
||||
@@ -398,4 +398,52 @@ defmodule BDS.GenerationTest do
|
||||
assert File.read!(Path.join([temp_dir, "html", "tag", "elixir", "index.html"])) =~ "Elixir"
|
||||
assert File.read!(Path.join([temp_dir, "html", "2026", "04", "index.html"])) =~ "2026-04"
|
||||
end
|
||||
|
||||
test "validate_site reports missing, extra, and stale generated pages and apply_validation repairs them",
|
||||
%{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 Post",
|
||||
content: "Validation body",
|
||||
language: "en"
|
||||
})
|
||||
|
||||
assert {:ok, published_post} = Posts.publish_post(post.id)
|
||||
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:core, :single])
|
||||
|
||||
post_path = BDS.Generation.post_output_path(published_post)
|
||||
index_path = Path.join([temp_dir, "html", "index.html"])
|
||||
post_file_path = Path.join([temp_dir, "html", post_path])
|
||||
extra_path = Path.join([temp_dir, "html", "obsolete.html"])
|
||||
|
||||
File.write!(index_path, "<html>tampered</html>")
|
||||
File.rm!(post_file_path)
|
||||
File.write!(extra_path, "<html>obsolete</html>")
|
||||
|
||||
assert {:ok, report} = BDS.Generation.validate_site(project.id)
|
||||
|
||||
assert "index.html" in report.stale_pages
|
||||
assert post_path in report.missing_pages
|
||||
assert "obsolete.html" in report.extra_pages
|
||||
|
||||
assert {:ok, repair} = BDS.Generation.apply_validation(project.id, [:core, :single])
|
||||
assert Enum.sort(repair.sections) == [:core, :single]
|
||||
|
||||
assert File.read!(index_path) != "<html>tampered</html>"
|
||||
assert File.exists?(post_file_path)
|
||||
refute File.exists?(extra_path)
|
||||
|
||||
assert {:ok, clean_report} = BDS.Generation.validate_site(project.id, [:core, :single])
|
||||
assert clean_report.missing_pages == []
|
||||
assert clean_report.extra_pages == []
|
||||
assert clean_report.stale_pages == []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -289,4 +289,42 @@ defmodule BDS.PreviewTest do
|
||||
|
||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||
end
|
||||
|
||||
test "preview query params can override the rendered theme for generated and draft pages", %{
|
||||
project: project
|
||||
} do
|
||||
assert {:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{
|
||||
public_url: "https://example.com/blog",
|
||||
main_language: "en",
|
||||
blog_languages: ["en"],
|
||||
pico_theme: "blue"
|
||||
})
|
||||
|
||||
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:core])
|
||||
|
||||
assert {:ok, post} =
|
||||
Posts.create_post(%{
|
||||
project_id: project.id,
|
||||
title: "Theme Draft",
|
||||
content: "Theme body",
|
||||
language: "en"
|
||||
})
|
||||
|
||||
assert {:ok, _server} = BDS.Preview.start_preview(project.id)
|
||||
|
||||
assert {:ok, %{body: generated_html, content_type: "text/html"}} =
|
||||
BDS.Preview.request(project.id, "/?theme=amber&mode=dark")
|
||||
|
||||
assert generated_html =~ ~s(data-theme="amber")
|
||||
assert generated_html =~ ~s(data-mode="dark")
|
||||
|
||||
assert {:ok, %{body: draft_html, content_type: "text/html"}} =
|
||||
BDS.Preview.preview_draft(project.id, "/draft/theme-draft?theme=amber&mode=dark", post.id)
|
||||
|
||||
assert draft_html =~ ~s(data-theme="amber")
|
||||
assert draft_html =~ ~s(data-mode="dark")
|
||||
|
||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user