diff --git a/src/routes/blog.rs b/src/routes/blog.rs index c91f87f..162151a 100644 --- a/src/routes/blog.rs +++ b/src/routes/blog.rs @@ -1,33 +1,17 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - -use ::rss::{ - extension::atom::{AtomExtension, Link}, - Category, Channel, Guid, Image, Item, -}; use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder}; use cached::proc_macro::once; -use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc}; -use chrono_tz::Europe; -use comrak::{parse_document, Arena}; use ramhorns::Content; use crate::{ config::Config, - utils::{ - date::Date, - markdown::{get_metadata, get_options, File, FileMetadataBlog, TypeFileMetadata}, - misc::{get_url, make_kw, read_file, Html}, - }, template::{InfosPage, NavBar}, + utils::{ + markdown::{File, TypeFileMetadata}, + misc::{make_kw, read_file, Html}, + routes::blog::{build_rss, get_post, get_posts, Post, BLOG_DIR, MIME_TYPE_RSS, POST_DIR}, + }, }; -const MIME_TYPE_RSS: &str = "application/rss+xml"; -const BLOG_DIR: &str = "blog"; -const POST_DIR: &str = "posts"; - #[get("/blog")] pub async fn index(config: web::Data) -> impl Responder { Html(build_index(config.get_ref().to_owned())) @@ -76,112 +60,6 @@ fn build_index(config: Config) -> String { ) } -#[derive(Content, Debug)] -struct Post { - title: String, - date: Date, - url: String, - desc: Option, - content: Option, - tags: Vec, -} - -impl Post { - // Fetch the file content - fn fetch_content(&mut self, data_dir: &str) { - let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); - let ext = ".md"; - - if let Some(file) = read_file( - &format!("{blog_dir}/{}{ext}", self.url), - &TypeFileMetadata::Blog, - ) { - self.content = Some(file.content); - } - } -} - -impl Hash for Post { - fn hash(&self, state: &mut H) { - if let Some(content) = &self.content { - content.hash(state); - } - } -} - -fn get_posts(location: &str) -> Vec { - let entries = std::fs::read_dir(location).map_or_else( - |_| vec![], - |res| { - res.flatten() - .filter(|f| f.path().extension().map_or(false, |ext| ext == "md")) - .collect::>() - }, - ); - - entries - .iter() - .filter_map(|f| { - let fname = f.file_name(); - let filename = fname.to_string_lossy(); - let file_without_ext = filename.split_at(filename.len() - 3).0; - - let file_metadata = std::fs::read_to_string(format!("{location}/{filename}")) - .map_or_else( - |_| FileMetadataBlog { - title: Some(file_without_ext.into()), - ..FileMetadataBlog::default() - }, - |text| { - let arena = Arena::new(); - - let options = get_options(); - let root = parse_document(&arena, &text, &options); - let mut metadata = - get_metadata(root, &TypeFileMetadata::Blog).blog.unwrap(); - - // Always have a title - metadata.title = metadata - .title - .map_or_else(|| Some(file_without_ext.into()), Some); - - metadata - }, - ); - - if file_metadata.publish == Some(true) { - Some(Post { - url: file_without_ext.into(), - title: file_metadata.title.unwrap(), - date: file_metadata.date.unwrap_or({ - let m = f.metadata().unwrap(); - let date = std::convert::Into::>::into( - m.modified().unwrap_or_else(|_| m.created().unwrap()), - ) - .date_naive(); - - Date { - day: date.day(), - month: date.month(), - year: date.year(), - } - }), - desc: file_metadata.description, - content: None, - tags: file_metadata - .tags - .unwrap_or_default() - .iter() - .map(|t| t.name.clone()) - .collect(), - }) - } else { - None - } - }) - .collect::>() -} - #[derive(Content, Debug)] struct BlogPostTemplate { navbar: NavBar, @@ -220,64 +98,6 @@ fn build_post(file: &str, config: Config) -> String { ) } -fn get_post( - post: &mut Option, - filename: &str, - name: &str, - data_dir: &str, -) -> (InfosPage, String) { - let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); - let ext = ".md"; - - *post = read_file( - &format!("{blog_dir}/{filename}{ext}"), - &TypeFileMetadata::Blog, - ); - - let default = ( - filename, - &format!("Blog d'{name}"), - Vec::new(), - String::new(), - ); - let (title, desc, tags, toc) = match post { - Some(data) => ( - match &data.metadata.info.blog.as_ref().unwrap().title { - Some(text) => text, - None => default.0, - }, - match &data.metadata.info.blog.as_ref().unwrap().description { - Some(desc) => desc, - None => default.1, - }, - match &data.metadata.info.blog.as_ref().unwrap().tags { - Some(tags) => tags.clone(), - None => default.2, - }, - match &data.metadata.info.blog.as_ref().unwrap().toc { - // TODO: Generate TOC - Some(true) => String::new(), - _ => default.3, - }, - ), - None => default, - }; - - ( - InfosPage { - title: Some(format!("Post: {title}")), - desc: Some(desc.clone()), - kw: Some(make_kw( - &["blog", "blogging", "write", "writing"] - .into_iter() - .chain(tags.iter().map(|t| t.name.as_str())) - .collect::>(), - )), - }, - toc, - ) -} - #[routes] #[get("/blog/blog.rss")] #[get("/blog/rss")] @@ -286,110 +106,3 @@ pub async fn rss(config: web::Data) -> impl Responder { .content_type(ContentType(MIME_TYPE_RSS.parse().unwrap())) .body(build_rss(config.get_ref().to_owned())) } - -#[once(time = 10800)] // 3h -fn build_rss(config: Config) -> String { - let mut posts = get_posts(&format!( - "{}/{}/{}", - config.locations.data_dir, BLOG_DIR, POST_DIR - )); - - // Sort from newest to oldest - posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day)); - posts.reverse(); - - // Only the 20 newest - let max = 20; - if posts.len() > max { - posts.drain(max..); - } - - let link_to_site = get_url(config.fc.clone()); - let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.clone()) { - Some(format!("{mail} ({name})")) - } else { - None - }; - let title = format!("Blog d'{}", config.fc.name.unwrap_or_default()); - let lang = "fr"; - let channel = Channel { - title: title.clone(), - link: link_to_site.clone(), - description: "Un fil qui parle d'informatique notamment".into(), - language: Some(lang.into()), - managing_editor: author.clone(), - webmaster: author, - pub_date: Some(Local::now().to_rfc2822()), - categories: ["blog", "blogging", "write", "writing"] - .iter() - .map(|&c| Category { - name: c.into(), - ..Category::default() - }) - .collect(), - generator: Some("ewp with rss crate".into()), - docs: Some("https://www.rssboard.org/rss-specification".into()), - image: Some(Image { - url: format!("{link_to_site}/icons/favicon-32x32.png"), - title: title.clone(), - link: link_to_site.clone(), - ..Image::default() - }), - items: posts - .iter_mut() - .map(|p| { - // Get post data - p.fetch_content(&config.locations.data_dir); - - // Build item - Item { - title: Some(p.title.clone()), - link: Some(format!("{}/blog/p/{}", link_to_site, p.url)), - description: p.content.clone(), - categories: p - .tags - .iter() - .map(|c| Category { - name: c.to_owned(), - ..Category::default() - }) - .collect(), - guid: Some(Guid { - value: format!("urn:hash:{}", { - let mut hasher = DefaultHasher::new(); - p.hash(&mut hasher); - hasher.finish() - }), - permalink: false, - }), - pub_date: Some( - NaiveDateTime::parse_from_str( - &format!("{}-{}-{} 13:12:00", p.date.day, p.date.month, p.date.year), - "%d-%m-%Y %H:%M:%S", - ) - .unwrap() - .and_local_timezone(Europe::Paris) - .unwrap() - .to_rfc2822(), - ), - ..Item::default() - } - }) - .collect(), - atom_ext: Some(AtomExtension { - links: vec![Link { - href: format!("{link_to_site}/blog/rss"), - rel: "self".into(), - hreflang: Some(lang.into()), - mime_type: Some(MIME_TYPE_RSS.into()), - title: Some(title), - length: None, - }], - }), - ..Channel::default() - }; - - std::str::from_utf8(&channel.write_to(Vec::new()).unwrap()) - .unwrap() - .into() -} diff --git a/src/utils/routes/blog.rs b/src/utils/routes/blog.rs new file mode 100644 index 0000000..1170297 --- /dev/null +++ b/src/utils/routes/blog.rs @@ -0,0 +1,299 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use ::rss::{ + extension::atom::{AtomExtension, Link}, + Category, Channel, Guid, Image, Item, +}; +use cached::proc_macro::once; +use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc}; +use chrono_tz::Europe; +use comrak::{parse_document, Arena}; +use ramhorns::Content; + +use crate::{ + config::Config, + template::InfosPage, + utils::{ + date::Date, + markdown::{get_metadata, get_options, File, FileMetadataBlog, TypeFileMetadata}, + misc::{get_url, make_kw, read_file}, + }, +}; + +pub const MIME_TYPE_RSS: &str = "application/rss+xml"; +pub const BLOG_DIR: &str = "blog"; +pub const POST_DIR: &str = "posts"; + +#[derive(Content, Debug)] +pub struct Post { + title: String, + pub date: Date, + pub url: String, + desc: Option, + content: Option, + tags: Vec, +} + +impl Post { + // Fetch the file content + fn fetch_content(&mut self, data_dir: &str) { + let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); + let ext = ".md"; + + if let Some(file) = read_file( + &format!("{blog_dir}/{}{ext}", self.url), + &TypeFileMetadata::Blog, + ) { + self.content = Some(file.content); + } + } +} + +impl Hash for Post { + fn hash(&self, state: &mut H) { + if let Some(content) = &self.content { + content.hash(state); + } + } +} + +pub fn get_posts(location: &str) -> Vec { + let entries = std::fs::read_dir(location).map_or_else( + |_| vec![], + |res| { + res.flatten() + .filter(|f| f.path().extension().map_or(false, |ext| ext == "md")) + .collect::>() + }, + ); + + entries + .iter() + .filter_map(|f| { + let fname = f.file_name(); + let filename = fname.to_string_lossy(); + let file_without_ext = filename.split_at(filename.len() - 3).0; + + let file_metadata = std::fs::read_to_string(format!("{location}/{filename}")) + .map_or_else( + |_| FileMetadataBlog { + title: Some(file_without_ext.into()), + ..FileMetadataBlog::default() + }, + |text| { + let arena = Arena::new(); + + let options = get_options(); + let root = parse_document(&arena, &text, &options); + let mut metadata = + get_metadata(root, &TypeFileMetadata::Blog).blog.unwrap(); + + // Always have a title + metadata.title = metadata + .title + .map_or_else(|| Some(file_without_ext.into()), Some); + + metadata + }, + ); + + if file_metadata.publish == Some(true) { + Some(Post { + url: file_without_ext.into(), + title: file_metadata.title.unwrap(), + date: file_metadata.date.unwrap_or({ + let m = f.metadata().unwrap(); + let date = std::convert::Into::>::into( + m.modified().unwrap_or_else(|_| m.created().unwrap()), + ) + .date_naive(); + + Date { + day: date.day(), + month: date.month(), + year: date.year(), + } + }), + desc: file_metadata.description, + content: None, + tags: file_metadata + .tags + .unwrap_or_default() + .iter() + .map(|t| t.name.clone()) + .collect(), + }) + } else { + None + } + }) + .collect::>() +} + +pub fn get_post( + post: &mut Option, + filename: &str, + name: &str, + data_dir: &str, +) -> (InfosPage, String) { + let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); + let ext = ".md"; + + *post = read_file( + &format!("{blog_dir}/{filename}{ext}"), + &TypeFileMetadata::Blog, + ); + + let default = ( + filename, + &format!("Blog d'{name}"), + Vec::new(), + String::new(), + ); + let (title, desc, tags, toc) = match post { + Some(data) => ( + match &data.metadata.info.blog.as_ref().unwrap().title { + Some(text) => text, + None => default.0, + }, + match &data.metadata.info.blog.as_ref().unwrap().description { + Some(desc) => desc, + None => default.1, + }, + match &data.metadata.info.blog.as_ref().unwrap().tags { + Some(tags) => tags.clone(), + None => default.2, + }, + match &data.metadata.info.blog.as_ref().unwrap().toc { + // TODO: Generate TOC + Some(true) => String::new(), + _ => default.3, + }, + ), + None => default, + }; + + ( + InfosPage { + title: Some(format!("Post: {title}")), + desc: Some(desc.clone()), + kw: Some(make_kw( + &["blog", "blogging", "write", "writing"] + .into_iter() + .chain(tags.iter().map(|t| t.name.as_str())) + .collect::>(), + )), + }, + toc, + ) +} + +#[once(time = 10800)] // 3h +pub fn build_rss(config: Config) -> String { + let mut posts = get_posts(&format!( + "{}/{}/{}", + config.locations.data_dir, BLOG_DIR, POST_DIR + )); + + // Sort from newest to oldest + posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day)); + posts.reverse(); + + // Only the 20 newest + let max = 20; + if posts.len() > max { + posts.drain(max..); + } + + let link_to_site = get_url(config.fc.clone()); + let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.clone()) { + Some(format!("{mail} ({name})")) + } else { + None + }; + let title = format!("Blog d'{}", config.fc.name.unwrap_or_default()); + let lang = "fr"; + let channel = Channel { + title: title.clone(), + link: link_to_site.clone(), + description: "Un fil qui parle d'informatique notamment".into(), + language: Some(lang.into()), + managing_editor: author.clone(), + webmaster: author, + pub_date: Some(Local::now().to_rfc2822()), + categories: ["blog", "blogging", "write", "writing"] + .iter() + .map(|&c| Category { + name: c.into(), + ..Category::default() + }) + .collect(), + generator: Some("ewp with rss crate".into()), + docs: Some("https://www.rssboard.org/rss-specification".into()), + image: Some(Image { + url: format!("{link_to_site}/icons/favicon-32x32.png"), + title: title.clone(), + link: link_to_site.clone(), + ..Image::default() + }), + items: posts + .iter_mut() + .map(|p| { + // Get post data + p.fetch_content(&config.locations.data_dir); + + // Build item + Item { + title: Some(p.title.clone()), + link: Some(format!("{}/blog/p/{}", link_to_site, p.url)), + description: p.content.clone(), + categories: p + .tags + .iter() + .map(|c| Category { + name: c.to_owned(), + ..Category::default() + }) + .collect(), + guid: Some(Guid { + value: format!("urn:hash:{}", { + let mut hasher = DefaultHasher::new(); + p.hash(&mut hasher); + hasher.finish() + }), + permalink: false, + }), + pub_date: Some( + NaiveDateTime::parse_from_str( + &format!("{}-{}-{} 13:12:00", p.date.day, p.date.month, p.date.year), + "%d-%m-%Y %H:%M:%S", + ) + .unwrap() + .and_local_timezone(Europe::Paris) + .unwrap() + .to_rfc2822(), + ), + ..Item::default() + } + }) + .collect(), + atom_ext: Some(AtomExtension { + links: vec![Link { + href: format!("{link_to_site}/blog/rss"), + rel: "self".into(), + hreflang: Some(lang.into()), + mime_type: Some(MIME_TYPE_RSS.into()), + title: Some(title), + length: None, + }], + }), + ..Channel::default() + }; + + std::str::from_utf8(&channel.write_to(Vec::new()).unwrap()) + .unwrap() + .into() +} diff --git a/src/utils/routes/mod.rs b/src/utils/routes/mod.rs index 87bb5a6..9cb7ea3 100644 --- a/src/utils/routes/mod.rs +++ b/src/utils/routes/mod.rs @@ -1 +1,2 @@ +pub mod blog; pub mod cours;