yggdrasil/api/posts/
outline.rs1use crate::api::error::ApiError;
8use leptos::prelude::*;
9
10use super::types::PostOutline;
11#[cfg(feature = "server")]
12use super::types::OUTLINE_PREVIEW_LIMIT;
13#[cfg(feature = "server")]
14use crate::api::error::AppError;
15#[cfg(feature = "server")]
16use crate::db::pool::get_conn;
17
18#[server(LatestPostOutline, endpoint = "latest_post_outline", input = leptos::server_fn::codec::Json)]
23pub async fn latest_post_outline() -> Result<Option<PostOutline>, ApiError> {
24 #[cfg(feature = "server")]
25 {
26 if let Some(cached) = crate::cache::get_latest_outline().await {
27 return Ok(cached);
28 }
29 let client = get_conn().await.map_err(AppError::db_conn)?;
30 let row = client
31 .query_opt(
32 "SELECT slug, toc_html FROM posts
33 WHERE status = 'published' AND deleted_at IS NULL
34 ORDER BY published_at DESC
35 LIMIT 1",
36 &[],
37 )
38 .await
39 .map_err(AppError::query)?;
40 let outline = row.and_then(|row| {
41 let toc: Option<String> = row.get("toc_html");
42 outline_from_toc(row.get("slug"), toc.as_deref().unwrap_or_default())
43 });
44 crate::cache::set_latest_outline(outline.clone()).await;
45 Ok(outline)
46 }
47
48 #[cfg(not(feature = "server"))]
49 {
50 Ok(None)
51 }
52}
53
54#[cfg(feature = "server")]
55fn outline_from_toc(slug: String, toc_html: &str) -> Option<PostOutline> {
56 let sections = crate::utils::toc::toc_sections(toc_html, OUTLINE_PREVIEW_LIMIT);
57 (sections.total > 0).then(|| PostOutline {
58 slug,
59 total: u32::try_from(sections.total).unwrap_or(u32::MAX),
60 headings: sections.headings,
61 })
62}
63
64#[cfg(all(test, feature = "server"))]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn article_without_headings_has_no_outline() {
70 assert_eq!(outline_from_toc("a".into(), ""), None);
71 }
72
73 #[test]
74 fn outline_keeps_six_headings_and_total() {
75 let md: String = (1..=8)
76 .map(|i| format!("## 第 {i} 节\n\n正文\n\n"))
77 .collect();
78 let toc = crate::api::markdown::render_markdown_enhanced(&md).toc_html;
79 let outline = outline_from_toc("post".into(), &toc).expect("有小节");
80 assert_eq!(outline.slug, "post");
81 assert_eq!(outline.total, 8);
82 assert_eq!(outline.headings.len(), OUTLINE_PREVIEW_LIMIT);
83 assert_eq!(outline.headings[0], "第 1 节");
84 }
85}