chore: source formatting and spec allignment

This commit is contained in:
2026-07-18 14:20:23 +02:00
parent a594b99e90
commit 16a210c0ad
119 changed files with 8868 additions and 5250 deletions

View File

@@ -2,11 +2,11 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::thread;
use axum::Router;
use axum::extract::{Path as AxumPath, Query, State};
use axum::http::{StatusCode, Uri, header};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use serde::Deserialize;
use tokio::sync::oneshot;
@@ -81,7 +81,9 @@ pub fn start_preview_server(
};
let listener = std::net::TcpListener::bind((PREVIEW_HOST, PREVIEW_PORT)).map_err(|error| {
if error.kind() == std::io::ErrorKind::AddrInUse {
EngineError::Conflict(format!("preview server already running on {PREVIEW_HOST}:{PREVIEW_PORT}"))
EngineError::Conflict(format!(
"preview server already running on {PREVIEW_HOST}:{PREVIEW_PORT}"
))
} else {
EngineError::Io(error)
}
@@ -135,23 +137,23 @@ async fn handle_draft_preview(
theme: query.theme.clone(),
mode: query.mode.clone(),
};
match render_preview_response(&state, &format!("/__draft/{post_id}"), query.language.as_deref(), Some(&style)) {
match render_preview_response(
&state,
&format!("/__draft/{post_id}"),
query.language.as_deref(),
Some(&style),
) {
Ok(response) => response,
Err(error) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
}
}
async fn handle_style_preview(
Query(query): Query<StylePreviewQuery>,
) -> Response {
async fn handle_style_preview(Query(query): Query<StylePreviewQuery>) -> Response {
let theme = query.theme.unwrap_or_else(|| "default".to_string());
let mode = query.mode.unwrap_or_else(|| "auto".to_string());
let html = format!(
"<!doctype html><html lang=\"en\" data-theme=\"{}\" data-mode=\"{}\"><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /><title>Style Preview</title><link rel=\"stylesheet\" href=\"/assets/pico.min.css\" /><link rel=\"stylesheet\" href=\"/assets/bds.css\" /></head><body><main><section class=\"style-preview\"><h1>Style Preview</h1><p>Theme: {}</p><p>Mode: {}</p></section></main></body></html>",
theme,
mode,
theme,
mode,
theme, mode, theme, mode,
);
Html(html).into_response()
}
@@ -163,7 +165,10 @@ fn render_preview_response(
style: Option<&StylePreviewQuery>,
) -> EngineResult<Response> {
if let Some(post_id) = path.strip_prefix("/__draft/") {
let html = apply_preview_style(render_draft_preview(state, post_id, requested_language)?, style);
let html = apply_preview_style(
render_draft_preview(state, post_id, requested_language)?,
style,
);
return Ok(Html(html).into_response());
}
@@ -178,8 +183,15 @@ fn render_preview_response(
.iter()
.map(|source| (source.post.clone(), source.body_markdown.clone()))
.collect::<Vec<_>>();
let response = build_preview_response(db.conn(), &state.data_dir, &state.project_id, &metadata, &input_posts, path)
.map_err(|error| EngineError::Parse(error.to_string()))?;
let response = build_preview_response(
db.conn(),
&state.data_dir,
&state.project_id,
&metadata,
&input_posts,
path,
)
.map_err(|error| EngineError::Parse(error.to_string()))?;
let status = StatusCode::from_u16(response.status_code).unwrap_or(StatusCode::OK);
Ok((status, Html(apply_preview_style(response.html, style))).into_response())
}
@@ -188,7 +200,9 @@ fn apply_preview_style(html: String, style: Option<&StylePreviewQuery>) -> Strin
let Some(style) = style else {
return html;
};
if style.theme.as_deref().unwrap_or("").is_empty() && style.mode.as_deref().unwrap_or("").is_empty() {
if style.theme.as_deref().unwrap_or("").is_empty()
&& style.mode.as_deref().unwrap_or("").is_empty()
{
return html;
}
@@ -238,34 +252,42 @@ fn render_draft_preview(
let db = Database::open(&state.db_path)?;
let metadata = crate::engine::meta::read_project_json(&state.data_dir)?;
let post = queries::post::get_post_by_id(db.conn(), post_id)?;
let canonical_language = post.language.as_deref().unwrap_or_else(|| metadata.main_language.as_deref().unwrap_or("en"));
let canonical_language = post
.language
.as_deref()
.unwrap_or_else(|| metadata.main_language.as_deref().unwrap_or("en"));
let target_language = requested_language.unwrap_or(canonical_language);
if target_language != canonical_language {
if let Ok(translation) = queries::post_translation::get_post_translation_by_post_and_language(
db.conn(),
post_id,
target_language,
) {
let mut translated_post = post.clone();
translated_post.title = translation.title.clone();
translated_post.excerpt = translation.excerpt.clone();
translated_post.language = Some(translation.language.clone());
translated_post.status = translation.status.clone();
translated_post.file_path = translation.file_path.clone();
translated_post.published_at = translation.published_at.or(post.published_at);
let body = load_translation_body(&state.data_dir, &translation)?;
let response = build_preview_response(
if target_language != canonical_language
&& let Ok(translation) =
queries::post_translation::get_post_translation_by_post_and_language(
db.conn(),
&state.data_dir,
&state.project_id,
&metadata,
&[(translated_post, body)],
&crate::render::build_canonical_post_path(&post, target_language, metadata.main_language.as_deref().unwrap_or("en")),
post_id,
target_language,
)
.map_err(|error| EngineError::Parse(error.to_string()))?;
return Ok(response.html);
}
{
let mut translated_post = post.clone();
translated_post.title = translation.title.clone();
translated_post.excerpt = translation.excerpt.clone();
translated_post.language = Some(translation.language.clone());
translated_post.status = translation.status.clone();
translated_post.file_path = translation.file_path.clone();
translated_post.published_at = translation.published_at.or(post.published_at);
let body = load_translation_body(&state.data_dir, &translation)?;
let response = build_preview_response(
db.conn(),
&state.data_dir,
&state.project_id,
&metadata,
&[(translated_post, body)],
&crate::render::build_canonical_post_path(
&post,
target_language,
metadata.main_language.as_deref().unwrap_or("en"),
),
)
.map_err(|error| EngineError::Parse(error.to_string()))?;
return Ok(response.html);
}
let body = load_post_body(&state.data_dir, &post)?;
@@ -275,7 +297,11 @@ fn render_draft_preview(
&state.project_id,
&metadata,
&[(post.clone(), body)],
&crate::render::build_canonical_post_path(&post, canonical_language, metadata.main_language.as_deref().unwrap_or("en")),
&crate::render::build_canonical_post_path(
&post,
canonical_language,
metadata.main_language.as_deref().unwrap_or("en"),
),
)
.map_err(|error| EngineError::Parse(error.to_string()))?;
Ok(response.html)
@@ -288,7 +314,10 @@ fn collect_published_posts(
let db = Database::open(&state.db_path)?;
let posts = queries::post::list_posts_by_project(db.conn(), &state.project_id)?;
let mut published = Vec::new();
for post in posts.into_iter().filter(|post| matches!(post.status, PostStatus::Published)) {
for post in posts
.into_iter()
.filter(|post| matches!(post.status, PostStatus::Published))
{
published.push(PublishedPostSource {
body_markdown: load_post_body(&state.data_dir, &post)?,
post,
@@ -318,7 +347,11 @@ fn load_translation_body(
load_markdown_body(data_dir, &translation.file_path, true)
}
fn load_markdown_body(data_dir: &Path, relative_path: &str, translation: bool) -> EngineResult<String> {
fn load_markdown_body(
data_dir: &Path,
relative_path: &str,
translation: bool,
) -> EngineResult<String> {
let raw = fs::read_to_string(data_dir.join(relative_path.trim_start_matches('/')))?;
let body = if translation {
read_translation_file(&raw).map(|(_, body)| body)
@@ -351,25 +384,32 @@ fn serve_scoped_file(
let scope_root = data_dir.join(scope_dir);
let candidate = scope_root.join(relative);
if !candidate.exists() || !candidate.is_file() {
return Ok(Some(error_response(StatusCode::NOT_FOUND, "preview asset not found")));
return Ok(Some(error_response(
StatusCode::NOT_FOUND,
"preview asset not found",
)));
}
let canonical_candidate = candidate.canonicalize()?;
let canonical_scope_root = scope_root.canonicalize().unwrap_or(scope_root);
if !canonical_candidate.starts_with(&canonical_scope_root) {
return Ok(Some(error_response(StatusCode::NOT_FOUND, "preview asset not found")));
return Ok(Some(error_response(
StatusCode::NOT_FOUND,
"preview asset not found",
)));
}
let bytes = fs::read(&canonical_candidate)?;
let mime = guess_content_type(&canonical_candidate);
Ok(Some((
StatusCode::OK,
[(header::CONTENT_TYPE, mime)],
bytes,
)
.into_response()))
Ok(Some(
(StatusCode::OK, [(header::CONTENT_TYPE, mime)], bytes).into_response(),
))
}
fn guess_content_type(path: &Path) -> &'static str {
match path.extension().and_then(|ext| ext.to_str()).unwrap_or_default() {
match path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or_default()
{
"css" => "text/css; charset=utf-8",
"js" => "application/javascript; charset=utf-8",
"json" => "application/json; charset=utf-8",
@@ -383,7 +423,12 @@ fn guess_content_type(path: &Path) -> &'static str {
}
fn error_response(status: StatusCode, message: &str) -> Response {
(status, [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], message.to_string()).into_response()
(
status,
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
message.to_string(),
)
.into_response()
}
#[cfg(test)]
@@ -391,7 +436,7 @@ mod tests {
use super::*;
use crate::db::queries;
use crate::engine::meta;
use crate::model::{Post, Project, ProjectMetadata, PostStatus};
use crate::model::{Post, PostStatus, Project, ProjectMetadata};
use std::sync::{Mutex, OnceLock};
fn preview_port_guard() -> &'static Mutex<()> {
@@ -504,9 +549,16 @@ mod tests {
#[test]
fn root_preview_renders_index_page() {
let db = Database::open_in_memory().unwrap();
let html = build_preview_response(db.conn(), Path::new("."), "project-1", &make_metadata(), &[(make_post().post, make_post().body_markdown)], "/")
.unwrap()
.html;
let html = build_preview_response(
db.conn(),
Path::new("."),
"project-1",
&make_metadata(),
&[(make_post().post, make_post().body_markdown)],
"/",
)
.unwrap()
.html;
assert!(html.contains("post-list"));
}
@@ -568,11 +620,15 @@ mod tests {
let client = reqwest::blocking::Client::new();
let mut body = None;
for _ in 0..20 {
if let Ok(response) = client.get(format!("http://{PREVIEW_HOST}:{PREVIEW_PORT}/__draft/post-1")).send() {
if response.status().is_success() {
body = Some(response.text().unwrap());
break;
}
if let Ok(response) = client
.get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/__draft/post-1"
))
.send()
&& response.status().is_success()
{
body = Some(response.text().unwrap());
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
@@ -599,7 +655,9 @@ mod tests {
let client = reqwest::blocking::Client::new();
let response = client
.get(format!("http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/../outside.txt"))
.get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/../outside.txt"
))
.send()
.unwrap();
server.stop().unwrap();
@@ -628,7 +686,9 @@ mod tests {
.send()
.unwrap();
let asset = client
.get(format!("http://{PREVIEW_HOST}:{PREVIEW_PORT}/assets/site.css"))
.get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/assets/site.css"
))
.send()
.unwrap();
let media_body = media.text().unwrap();
@@ -739,7 +799,10 @@ mod tests {
dir.path(),
"project-1",
&make_metadata(),
&[(hidden.post.clone(), hidden.body_markdown.clone()), (featured.post.clone(), featured.body_markdown.clone())],
&[
(hidden.post.clone(), hidden.body_markdown.clone()),
(featured.post.clone(), featured.body_markdown.clone()),
],
"/category/hidden",
)
.unwrap();
@@ -750,7 +813,10 @@ mod tests {
dir.path(),
"project-1",
&make_metadata(),
&[(hidden.post, hidden.body_markdown), (featured.post, featured.body_markdown)],
&[
(hidden.post, hidden.body_markdown),
(featured.post, featured.body_markdown),
],
"/category/featured",
)
.unwrap();
@@ -893,6 +959,10 @@ mod tests {
.unwrap();
assert_eq!(response.status_code, 404);
assert!(response.html.contains("No rendered page exists for /missing/route"));
assert!(
response
.html
.contains("No rendered page exists for /missing/route")
);
}
}
}