init elas atelier
This commit is contained in:
@@ -11,7 +11,7 @@ use crate::{
|
||||
AppState,
|
||||
auth::is_authed,
|
||||
error::AppError,
|
||||
models::{CreatePostRequest, PostDetail, PostInfo, PostMeta},
|
||||
models::{CoverImage, CreatePostRequest, PostDetail, PostInfo, PostMeta},
|
||||
};
|
||||
|
||||
const WORDS_PER_MINUTE: u32 = 200;
|
||||
@@ -114,6 +114,59 @@ fn reading_time(body: &str) -> u32 {
|
||||
(words + WORDS_PER_MINUTE - 1) / WORDS_PER_MINUTE.max(1)
|
||||
}
|
||||
|
||||
/// Scan markdown for `` images. Returns (alt, url) pairs in order.
|
||||
/// Skips inside fenced code blocks. Tolerates titles like ``.
|
||||
fn extract_images(body: &str) -> Vec<(String, String)> {
|
||||
let mut out = Vec::new();
|
||||
let mut in_fence = false;
|
||||
for line in body.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
|
||||
in_fence = !in_fence;
|
||||
continue;
|
||||
}
|
||||
if in_fence {
|
||||
continue;
|
||||
}
|
||||
let bytes = line.as_bytes();
|
||||
let mut i = 0;
|
||||
while i + 1 < bytes.len() {
|
||||
if bytes[i] == b'!' && bytes[i + 1] == b'[' {
|
||||
if let Some(rel_close) = line[i + 2..].find(']') {
|
||||
let close = i + 2 + rel_close;
|
||||
if close + 1 < line.len() && bytes[close + 1] == b'(' {
|
||||
if let Some(rel_paren) = line[close + 2..].find(')') {
|
||||
let paren_end = close + 2 + rel_paren;
|
||||
let alt = line[i + 2..close].to_string();
|
||||
let url_field = line[close + 2..paren_end].trim();
|
||||
let url = url_field
|
||||
.split_once(|c: char| c.is_whitespace())
|
||||
.map(|(u, _)| u)
|
||||
.unwrap_or(url_field)
|
||||
.trim_matches(|c| c == '<' || c == '>')
|
||||
.to_string();
|
||||
if !url.is_empty() {
|
||||
out.push((alt, url));
|
||||
}
|
||||
i = paren_end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn cover_from(images: &[(String, String)]) -> Option<CoverImage> {
|
||||
images.first().map(|(alt, url)| CoverImage {
|
||||
url: url.clone(),
|
||||
alt: alt.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn excerpt_from(meta: &PostMeta, body: &str) -> String {
|
||||
if let Some(s) = meta.summary.as_ref() {
|
||||
if !s.trim().is_empty() {
|
||||
@@ -191,6 +244,13 @@ pub async fn create_post(
|
||||
}
|
||||
}
|
||||
|
||||
let images = extract_images(&payload.content);
|
||||
if images.is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"A gallery entry must include at least one image ( in the markdown body).".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let meta = PostMeta {
|
||||
date: payload.date.unwrap_or_else(|| Utc::now().date_naive()),
|
||||
title: payload.title.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()),
|
||||
@@ -202,6 +262,8 @@ pub async fn create_post(
|
||||
write_post_atomic(&state, &slug, &contents).await?;
|
||||
|
||||
info!("Post saved: {}", slug);
|
||||
let image_count = images.len() as u32;
|
||||
let cover = cover_from(&images);
|
||||
Ok(Json(PostDetail {
|
||||
slug,
|
||||
date: meta.date,
|
||||
@@ -211,6 +273,8 @@ pub async fn create_post(
|
||||
draft: meta.draft,
|
||||
reading_time: reading_time(&payload.content),
|
||||
content: payload.content,
|
||||
cover_image: cover,
|
||||
image_count,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -271,6 +335,7 @@ pub async fn list_posts(
|
||||
if meta.draft && !admin {
|
||||
continue;
|
||||
}
|
||||
let images = extract_images(&body);
|
||||
posts.push(PostInfo {
|
||||
slug: slug.to_string(),
|
||||
date: meta.date,
|
||||
@@ -280,6 +345,8 @@ pub async fn list_posts(
|
||||
draft: meta.draft,
|
||||
reading_time: reading_time(&body),
|
||||
excerpt: excerpt_from(&meta, &body),
|
||||
cover_image: cover_from(&images),
|
||||
image_count: images.len() as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -305,6 +372,7 @@ pub async fn get_post(
|
||||
return Err(AppError::NotFound("Post not found".to_string()));
|
||||
}
|
||||
|
||||
let images = extract_images(&body);
|
||||
Ok(Json(PostDetail {
|
||||
slug,
|
||||
date: meta.date,
|
||||
@@ -314,5 +382,7 @@ pub async fn get_post(
|
||||
draft: meta.draft,
|
||||
reading_time: reading_time(&body),
|
||||
content: body,
|
||||
cover_image: cover_from(&images),
|
||||
image_count: images.len() as u32,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user