Blog attempt 5

Clean up

+190 -153
+5
clippy.toml
··· 1 + module-item-order-groupings = [ 2 + [ "modules", [ "extern_crate", "mod", "foreign_mod" ], ], 3 + [ "use", [ "use", ], ], 4 + [ "everything_else", [ "macro", "global_asm", "static", "const", "ty_alias", "enum", "struct", "union", "trait", "trait_alias", "impl", "fn", ], ], 5 + ]
+4 -1
src/db.rs
··· 41 41 42 42 const MIGRATIONS: Migrations<'_> = Migrations::from_slice(MIGRATIONS_SLICE); 43 43 44 - #[allow(clippy::expect_used)] 44 + #[expect( 45 + clippy::expect_used, 46 + reason = "If we fail to get DB, we might as well panic." 47 + )] 45 48 pub fn connect() -> Connection { 46 49 let mut conn = Connection::open("./db.db3").expect("Failed to open database file"); 47 50
+18 -20
src/error.rs
··· 1 - use std::fmt; 2 - 3 - #[allow(unused_imports)] 4 - use log::{debug, error, info, warn}; 1 + use log::error; 5 2 use poem::{Response, error::ResponseError, http::StatusCode}; 3 + use std::fmt; 6 4 7 5 pub enum AppError { 6 + DatabaseError(String), 7 + InternalServerError(String), 8 8 NotFound, 9 9 Unauthorized, 10 - DatabaseError(String), 11 - InternalServerError(String), 12 10 } 13 11 14 12 impl AppError { 15 - pub fn internal_server_error(msg: String) -> Self { 16 - error!("Internal server error: {msg}"); 17 - AppError::InternalServerError(msg) 18 - } 19 - 20 13 pub fn database_error(msg: String) -> Self { 21 14 error!("Database error: {msg}"); 22 15 AppError::DatabaseError(msg) 23 16 } 17 + 18 + pub fn internal_server_error(msg: String) -> Self { 19 + error!("Internal server error: {msg}"); 20 + AppError::InternalServerError(msg) 21 + } 24 22 } 25 23 26 24 impl fmt::Display for AppError { ··· 43 41 impl std::error::Error for AppError {} 44 42 45 43 impl ResponseError for AppError { 46 - fn status(&self) -> StatusCode { 47 - match self { 48 - AppError::NotFound => StatusCode::NOT_FOUND, 49 - AppError::Unauthorized => StatusCode::UNAUTHORIZED, 50 - _ => StatusCode::INTERNAL_SERVER_ERROR, 51 - } 52 - } 53 - 54 44 fn as_response(&self) -> Response { 55 45 let body = match self { 56 46 AppError::NotFound => "404 Not Found", ··· 60 50 61 51 Response::builder().status(self.status()).body(body) 62 52 } 53 + 54 + fn status(&self) -> StatusCode { 55 + match self { 56 + AppError::NotFound => StatusCode::NOT_FOUND, 57 + AppError::Unauthorized => StatusCode::UNAUTHORIZED, 58 + _ => StatusCode::INTERNAL_SERVER_ERROR, 59 + } 60 + } 63 61 } 64 62 65 63 impl From<rusqlite::Error> for AppError { ··· 97 95 98 96 impl<T> From<std::sync::PoisonError<T>> for AppError { 99 97 fn from(_: std::sync::PoisonError<T>) -> Self { 100 - AppError::internal_server_error("Lock poisoned".to_string()) 98 + AppError::internal_server_error("Lock poisoned".to_owned()) 101 99 } 102 100 } 103 101
+7 -10
src/feed.rs
··· 2 2 use chrono::Utc; 3 3 use poem::{IntoResponse, handler, web::Data}; 4 4 5 - use crate::{ 6 - D, W, 7 - post::{fetch_all_posts, render::render_post_nocss}, 8 - }; 5 + use crate::{D, W, post}; 9 6 10 7 const HOST: &str = "https://j0.lol"; 11 8 9 + #[expect(clippy::expect_used, reason = "Feed reader is low-stakes")] 12 10 fn entries(conn: &W) -> Vec<Entry> { 13 - let posts = fetch_all_posts(conn).unwrap_or_default(); 11 + let posts = post::fetch::all(conn).unwrap_or_default(); 14 12 15 13 let entries: Vec<_> = posts 16 14 .iter() ··· 21 19 ..Default::default() 22 20 }; 23 21 24 - #[allow(clippy::expect_used)] 25 - let contents = render_post_nocss(&post.contents).expect("HTML parse fail in feed"); 22 + let contents = post::render::no_css(&post.contents).expect("HTML parse fail in feed"); 26 23 27 24 entry 28 25 .link(Link { ··· 53 50 .links(vec![ 54 51 Link { 55 52 href: format!("{HOST}/blog"), 56 - rel: "alternate".to_string(), 53 + rel: "alternate".to_owned(), 57 54 ..Default::default() 58 55 }, 59 56 Link { 60 57 href: format!("{HOST}/feed"), 61 - rel: "self".to_string(), 58 + rel: "self".to_owned(), 62 59 ..Default::default() 63 60 }, 64 61 ]) ··· 67 64 name: "Jo Null".to_owned(), 68 65 ..Default::default() 69 66 }) 70 - .base(HOST.to_string()) 67 + .base(HOST.to_owned()) 71 68 .icon(format!("{HOST}/static/favicon.ico")) 72 69 .logo(format!("{HOST}/static/j0site-banner.png")) 73 70 .updated(Utc::now())
+44 -18
src/main.rs
··· 1 - // warn on clippy pedantic 2 - #![deny(clippy::unwrap_used, clippy::expect_used)] 3 - #![warn(clippy::pedantic)] 1 + // i'm a pedant, sorry. 2 + #![deny( 3 + clippy::unwrap_used, 4 + clippy::expect_used, 5 + clippy::allow_attributes, 6 + clippy::empty_enum_variants_with_brackets, 7 + clippy::empty_structs_with_brackets, 8 + clippy::error_impl_error, 9 + clippy::if_then_some_else_none, 10 + clippy::impl_trait_in_params, 11 + clippy::indexing_slicing, 12 + clippy::map_err_ignore, 13 + clippy::mod_module_files, 14 + clippy::mutex_atomic, 15 + clippy::mutex_integer, 16 + clippy::needless_raw_strings, 17 + clippy::str_to_string, 18 + clippy::try_err, 19 + clippy::unnecessary_self_imports, 20 + clippy::unused_trait_names, 21 + clippy::pub_use 22 + )] 23 + #![warn( 24 + clippy::pedantic, 25 + clippy::arbitrary_source_item_ordering, 26 + clippy::module_name_repetitions, 27 + clippy::pathbuf_init_then_push 28 + )] 4 29 #![allow( 5 30 clippy::missing_errors_doc, 6 31 clippy::missing_panics_doc, ··· 17 42 18 43 use crate::{ 19 44 feed::feed as feed_handler, 20 - og_image::og_image_handler, 21 45 other_pages::{contact, index, projects}, 22 - post::{ 23 - edit_post, list_posts, login, new_post, render_draft, submit_edited_post, submit_new_post, 24 - update_draft, view_post, 25 - }, 26 46 }; 27 47 use poem::{ 28 - EndpointExt, Route, Server, endpoint::StaticFilesEndpoint, get, listener::TcpListener, 48 + EndpointExt as _, Route, Server, endpoint::StaticFilesEndpoint, get, listener::TcpListener, 29 49 middleware::CookieJarManager, web::Data, 30 50 }; 31 51 use rusqlite::Connection; ··· 48 68 .at("/", get(index)) 49 69 .at("/contact", get(contact)) 50 70 .at("/projects", get(projects)) 51 - .at("/blog", get(list_posts)) 52 - .at("/blog/new", get(new_post).post(submit_new_post)) 53 - .at("/blog/new/sync", poem::post(update_draft)) 54 - .at("/blog/edit/render", poem::post(render_draft)) 55 - .at("/blog/edit/:slug", get(edit_post).post(submit_edited_post)) 56 - .at("/blog/:slug", get(view_post)) 57 - .at("/og-image/:slug", get(og_image_handler)) 58 - .at("/login", poem::post(login)) 71 + .at("/blog", get(post::list::list)) 72 + .at( 73 + "/blog/new", 74 + get(post::admin::new_post).post(post::admin::submit_new_post), 75 + ) 76 + .at("/blog/new/sync", poem::post(post::admin::update_draft)) 77 + .at("/blog/edit/render", poem::post(post::admin::render_draft)) 78 + .at( 79 + "/blog/edit/:slug", 80 + get(post::admin::edit_post).post(post::admin::submit_edited_post), 81 + ) 82 + .at("/blog/:slug", get(post::view::view)) 83 + .at("/og-image/:slug", get(og_image::og_image_handler)) 84 + .at("/login", poem::post(post::admin::login)) 59 85 .at("/feed", get(feed_handler)) 60 86 .nest("/static", StaticFilesEndpoint::new("./static/")) 61 87 .nest("/dist", StaticFilesEndpoint::new(env!("OUT_DIR"))) 62 88 .with(CookieJarManager::new()) 63 89 .data(conn.clone()); 64 90 65 - let port = env::var("PORT").unwrap_or("3000".to_string()); 91 + let port = env::var("PORT").unwrap_or("3000".to_owned()); 66 92 67 93 println!("Listening on https://localhost:{port}"); 68 94 Server::new(TcpListener::bind(format!("0.0.0.0:{port}")))
+47 -39
src/og_image.rs
··· 1 + mod cache { 2 + use super::{HashMap, Mutex, MutexGuard, OgImageData}; 3 + 4 + type Cache = HashMap<String, Vec<u8>>; 5 + 6 + // Simple in-memory cache for rendered images 7 + static IMAGE_CACHE: std::sync::OnceLock<Mutex<Cache>> = std::sync::OnceLock::new(); 8 + 9 + pub(super) fn acquire() -> &'static Mutex<Cache> { 10 + IMAGE_CACHE.get_or_init(|| Mutex::new(HashMap::new())) 11 + } 12 + 13 + pub(super) fn get(cache: &MutexGuard<'_, Cache>, key: &OgImageData) -> Option<Vec<u8>> { 14 + cache.get(&cache_key(key)).cloned() 15 + } 16 + 17 + pub(super) fn insert(cache: &mut MutexGuard<'_, Cache>, key: &OgImageData, val: Vec<u8>) { 18 + cache.insert(cache_key(key), val); 19 + } 20 + 21 + fn cache_key(data: &OgImageData) -> String { 22 + format!( 23 + "{}|{}|{}", 24 + data.title, 25 + data.subtitle.clone().unwrap_or_default(), 26 + data.datestring 27 + ) 28 + } 29 + } 30 + 1 31 use crate::{ 2 32 D, W, 3 33 error::{AppError, Result}, 4 - post::{fetch::fetch_post, format_date}, 34 + post::{fetch::one, format_date}, 5 35 }; 6 36 use derive_typst_intoval::{IntoDict, IntoValue}; 7 - use poem::{Body, IntoResponse, Response, handler}; 37 + use poem::{Body, IntoResponse as _, Response, handler}; 8 38 use serde::Serialize; 9 39 use std::{ 10 40 collections::HashMap, 11 41 sync::{Mutex, MutexGuard}, 12 42 }; 13 43 use typst::{ 14 - foundations::{Dict, IntoValue}, 44 + foundations::{Dict, IntoValue as _}, 15 45 layout::PagedDocument, 16 46 }; 17 47 use typst_as_lib::TypstEngine; ··· 27 57 28 58 #[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq, IntoDict, IntoValue)] 29 59 pub struct OgImageData { 30 - title: String, 31 - subtitle: Option<String>, 32 60 datestring: String, 61 + subtitle: Option<String>, 62 + title: String, 33 63 } 34 64 35 65 impl From<OgImageData> for Dict { ··· 56 86 let document: PagedDocument = template.compile_with_input(data.clone()).output?; 57 87 assert!(document.pages.len() == 1); 58 88 59 - let png = typst_render::render(&document.pages[0], 4.0) 60 - .encode_png() 61 - .map_err(|e| AppError::internal_server_error(format!("Typst compile error: {e}")))?; 89 + let png = typst_render::render( 90 + document 91 + .pages 92 + .first() 93 + .ok_or(AppError::internal_server_error( 94 + "Typst compile error: No page".to_owned(), 95 + ))?, 96 + 4.0, 97 + ) 98 + .encode_png() 99 + .map_err(|e| AppError::internal_server_error(format!("Typst compile error: {e}")))?; 62 100 63 101 // Cache the result 64 102 { ··· 75 113 poem::web::Path(slug): poem::web::Path<String>, 76 114 poem::web::Data(conn): D<&W>, 77 115 ) -> Result<Response> { 78 - let post = fetch_post(slug, conn)?; 116 + let post = one(slug, conn)?; 79 117 80 118 let og_image_data = OgImageData { 81 119 title: post.title, ··· 90 128 .body(Body::from_vec(image_bytes)) 91 129 .into_response()) 92 130 } 93 - 94 - mod cache { 95 - use super::{HashMap, Mutex, MutexGuard, OgImageData}; 96 - 97 - type Cache = HashMap<String, Vec<u8>>; 98 - 99 - // Simple in-memory cache for rendered images 100 - static IMAGE_CACHE: std::sync::OnceLock<Mutex<Cache>> = std::sync::OnceLock::new(); 101 - 102 - pub(super) fn acquire() -> &'static Mutex<Cache> { 103 - IMAGE_CACHE.get_or_init(|| Mutex::new(HashMap::new())) 104 - } 105 - 106 - pub(super) fn get(cache: &MutexGuard<'_, Cache>, key: &OgImageData) -> Option<Vec<u8>> { 107 - cache.get(&cache_key(key)).cloned() 108 - } 109 - 110 - pub(super) fn insert(cache: &mut MutexGuard<'_, Cache>, key: &OgImageData, val: Vec<u8>) { 111 - cache.insert(cache_key(key), val); 112 - } 113 - 114 - fn cache_key(data: &OgImageData) -> String { 115 - format!( 116 - "{}|{}|{}", 117 - data.title, 118 - data.subtitle.clone().unwrap_or_default(), 119 - data.datestring 120 - ) 121 - } 122 - }
+1 -1
src/other_pages.rs
··· 4 4 }; 5 5 use maud::{Markup, PreEscaped, html}; 6 6 use poem::handler; 7 - use rand::seq::SliceRandom; 7 + use rand::seq::SliceRandom as _; 8 8 9 9 #[handler] 10 10 pub fn index() -> Markup {
+10 -13
src/post.rs
··· 6 6 7 7 use std::fs::read_to_string; 8 8 9 - pub use admin::render_draft; 10 - pub use admin::{edit_post, login, new_post, submit_edited_post, submit_new_post, update_draft}; 11 - use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDateTime, Utc}; 12 - pub use fetch::fetch_all_posts; 13 - pub use list::list_posts; 9 + use chrono::{DateTime, Datelike as _, FixedOffset, Local, NaiveDateTime, Utc}; 14 10 use maud::{Markup, html}; 15 - pub use render::render_post; 16 11 use serde::Deserialize; 17 - pub use view::view_post; 18 12 const ISO8601_DATE: &str = "%Y-%m-%dT%H:%M"; 19 13 20 14 #[derive(Deserialize, Debug)] 21 15 pub struct Post { 22 - pub title: String, 16 + pub bsky_uri: Option<String>, 17 + pub category: Option<String>, 23 18 pub contents: String, 19 + pub creation_datetime: DateTime<Local>, 24 20 pub slug: String, 25 21 pub subtitle: Option<String>, 26 - pub category: Option<String>, 27 - pub bsky_uri: Option<String>, 28 - pub creation_datetime: DateTime<Local>, 22 + pub title: String, 29 23 } 30 24 31 25 #[must_use] ··· 65 59 } 66 60 67 61 #[must_use] 68 - #[allow(clippy::expect_used)] 62 + #[expect( 63 + clippy::expect_used, 64 + reason = "We need the secret for progam execution" 65 + )] 69 66 pub fn read_secret() -> String { 70 67 read_to_string("./.secret") 71 68 .expect("Failed to read secret file") 72 69 .trim() 73 - .to_string() 70 + .to_owned() 74 71 } 75 72 76 73 #[must_use]
+24 -24
src/post/admin.rs
··· 1 1 use chrono::DateTime; 2 2 use maud::{Markup, PreEscaped, html}; 3 3 use poem::{ 4 - IntoResponse, Response, handler, 4 + IntoResponse as _, Response, handler, 5 5 web::{Data, Form, Json, Redirect, cookie::CookieJar}, 6 6 }; 7 7 use serde::Deserialize; 8 8 9 9 use super::{ 10 10 ISO8601_DATE, Post, clean_empty_string, 11 - fetch::{fetch_draft, fetch_post}, 11 + fetch::{one, one_draft}, 12 12 parse_date, read_secret, 13 13 }; 14 14 use crate::{ 15 15 D, W, 16 16 error::{AppError, Result}, 17 - post::render_post, 17 + post, 18 18 template::header_extra, 19 19 }; 20 20 21 21 struct PostData { 22 - title: String, 22 + bsky_uri: Option<String>, 23 + category: Option<String>, 23 24 contents: String, 25 + creation_datetime: DateTime<chrono::Local>, 24 26 slug: String, 25 27 subtitle: Option<String>, 26 - category: Option<String>, 27 - bsky_uri: Option<String>, 28 - creation_datetime: DateTime<chrono::Local>, 28 + title: String, 29 29 } 30 30 31 31 #[derive(Copy, Clone)] ··· 37 37 38 38 #[derive(Deserialize)] 39 39 pub struct SubmitNewPostForm { 40 - pub title: String, 40 + pub bsky_uri: Option<String>, 41 + pub category: Option<String>, 41 42 pub contents: String, 43 + pub creation_datetime: String, 42 44 pub slug: String, 43 45 pub subtitle: Option<String>, 44 - pub category: Option<String>, 45 - pub bsky_uri: Option<String>, 46 - pub creation_datetime: String, 46 + pub title: String, 47 47 } 48 48 49 49 #[derive(Deserialize)] ··· 53 53 54 54 #[derive(Deserialize)] 55 55 pub struct SubmitEditedPostForm { 56 - pub title: String, 56 + pub bsky_uri: Option<String>, 57 + pub category: Option<String>, 57 58 pub contents: String, 59 + pub creation_datetime: String, 58 60 pub slug: String, 59 61 pub subtitle: Option<String>, 60 - pub category: Option<String>, 61 - pub bsky_uri: Option<String>, 62 - pub creation_datetime: String, 62 + pub title: String, 63 63 } 64 64 65 65 fn check_auth(cookie_jar: &CookieJar) -> Result<()> { ··· 71 71 } 72 72 73 73 #[handler] 74 - pub fn login(body: String, cookie_jar: &CookieJar) -> impl IntoResponse { 74 + pub fn login(body: String, cookie_jar: &CookieJar) -> Result<Response> { 75 75 cookie_jar.add(poem::web::cookie::Cookie::new_with_str("secret_pass", body)); 76 - #[allow(clippy::expect_used)] 77 76 let secret_value = cookie_jar 78 77 .get("secret_pass") 79 - .expect("Cookie should exist after being set"); 80 - html! {(secret_value)}.into_response() 78 + .ok_or(AppError::internal_server_error( 79 + "Cookie should exist after being set".to_owned(), 80 + ))?; 81 + Ok(html! {(secret_value)}.into_response()) 81 82 } 82 83 83 84 #[handler] 84 85 pub fn new_post(cookie_jar: &CookieJar, Data(conn): D<&W>) -> Result<Response> { 85 86 check_auth(cookie_jar)?; 86 87 87 - let post = fetch_draft(conn) 88 - .map_err(|_| AppError::internal_server_error("Could not fetch draft".to_string()))?; 88 + let post = one_draft(conn)?; 89 89 90 90 let response = render_post_form(&post, "POST", "/blog/new", "new"); 91 91 Ok(response.into_response()) ··· 99 99 ) -> Result<Response> { 100 100 check_auth(cookie_jar)?; 101 101 102 - let post = fetch_post(slug.to_string(), conn)?; 102 + let post = one(slug.to_string(), conn)?; 103 103 let response = render_post_form(&post, "POST", &format!("/blog/edit/{slug}"), "edit"); 104 104 Ok(response.into_response()) 105 105 } ··· 238 238 239 239 save_post(conn, &post_data, PostOperation::UpdateDraft)?; 240 240 241 - let rendered = render_post(&form.contents)?; 241 + let rendered = post::render::render(&form.contents)?; 242 242 Ok(rendered.into_response()) 243 243 } 244 244 ··· 246 246 pub fn render_draft(cookie_jar: &CookieJar, Json(form): Json<RenderDraftForm>) -> Result<Response> { 247 247 check_auth(cookie_jar)?; 248 248 249 - let rendered = render_post(&form.contents)?; 249 + let rendered = post::render::render(&form.contents)?; 250 250 Ok(rendered.into_response()) 251 251 } 252 252
+4 -4
src/post/fetch.rs
··· 5 5 }; 6 6 7 7 pub enum PostSource { 8 - Post(String), 9 8 Draft, 9 + Post(String), 10 10 } 11 11 12 - pub fn fetch_all_posts(conn: &W) -> Result<Vec<Post>> { 12 + pub fn all(conn: &W) -> Result<Vec<Post>> { 13 13 let conn = conn.lock()?; 14 14 15 15 let mut stmt = conn.prepare("SELECT title, contents, slug, subtitle, category, bsky_uri, creation_datetime FROM post") ··· 39 39 Ok(posts) 40 40 } 41 41 42 - pub fn fetch_post(slug: String, conn: &W) -> Result<Post> { 42 + pub fn one(slug: String, conn: &W) -> Result<Post> { 43 43 fetch_post_inner(&PostSource::Post(slug), conn) 44 44 } 45 45 46 - pub fn fetch_draft(conn: &W) -> Result<Post> { 46 + pub fn one_draft(conn: &W) -> Result<Post> { 47 47 fetch_post_inner(&PostSource::Draft, conn) 48 48 } 49 49
+9 -9
src/post/list.rs
··· 1 - use maud::{PreEscaped, html}; 2 - use poem::{IntoResponse, Response, handler, web::Data}; 3 - 4 - use super::{clock_icon, fetch::fetch_all_posts, format_date}; 1 + use super::clock_icon; 5 2 use crate::{ 6 3 D, W, 7 4 error::Result, 5 + post, 8 6 template::{footer, header_extra, navbar}, 9 7 }; 8 + use maud::{PreEscaped, html}; 9 + use poem::{IntoResponse as _, Response, handler, web::Data}; 10 10 11 11 #[handler] 12 - pub fn list_posts(Data(conn): D<&W>) -> Result<Response> { 13 - let posts = fetch_all_posts(conn)?; 12 + pub fn list(Data(conn): D<&W>) -> Result<Response> { 13 + let posts = post::fetch::all(conn)?; 14 14 15 15 let markup = html! { 16 16 ( header_extra(&html! { ··· 31 31 span { 32 32 (clock_icon()) 33 33 } 34 - time datetime=(post.creation_datetime) { (format_date(post.creation_datetime)) } 34 + time datetime=(post.creation_datetime) { (post::format_date(post.creation_datetime)) } 35 35 } 36 36 } 37 37 } 38 38 39 39 h2 { "Trash" } 40 40 ul { 41 - @for post in posts.iter().filter(|p| p.category == Some("trash".to_string())) { 41 + @for post in posts.iter().filter(|p| p.category == Some("trash".to_owned())) { 42 42 li { 43 43 a href={"/blog/" (post.slug) } { (PreEscaped(post.title.clone())) } 44 44 br; 45 45 span { 46 46 (clock_icon()) 47 47 } 48 - time datetime=(post.creation_datetime) { (format_date(post.creation_datetime)) } 48 + time datetime=(post.creation_datetime) { (post::format_date(post.creation_datetime)) } 49 49 } 50 50 } 51 51
+8 -5
src/post/render.rs
··· 5 5 error::{AppError, Result}, 6 6 template::{SpeechCharacter, SpeechEmotion}, 7 7 }; 8 - use autumnus::{formatter::Formatter, languages::Language}; 8 + use autumnus::{formatter::Formatter as _, languages::Language}; 9 9 use makup::Rewriter; 10 10 use maud::{Markup, PreEscaped, html}; 11 11 use std::collections::HashMap; ··· 62 62 .into_string() 63 63 } 64 64 65 + #[expect( 66 + clippy::expect_used, 67 + clippy::unwrap_used, 68 + reason = "Function is not allowed to error, annoyingly." 69 + )] 65 70 pub(crate) fn maud_code_block(text: &str, lang: &str) -> Markup { 66 71 let formatter = autumnus::HtmlInlineBuilder::new() 67 72 .lang(Language::guess(lang, text)) ··· 84 89 } 85 90 } 86 91 87 - // Safety: function is not allowed to error. Annoyingly. 88 - #[allow(clippy::expect_used, clippy::unwrap_used)] 89 92 fn code_block(contents: &str, attrs: HashMap<&str, &str>) -> String { 90 93 let lang = attrs 91 94 .get("lang") ··· 116 119 .into_string() 117 120 } 118 121 119 - pub fn render_post(markup: &str) -> Result<String> { 122 + pub fn render(markup: &str) -> Result<String> { 120 123 let mut rewriter = Rewriter::default(); 121 124 rewriter.add_component("speech-box", speech_box); 122 125 rewriter.add_component("code-block", code_block); ··· 128 131 Ok(contents) 129 132 } 130 133 131 - pub fn render_post_nocss(markup: &str) -> Result<String> { 134 + pub fn no_css(markup: &str) -> Result<String> { 132 135 let mut rewriter = Rewriter::default(); 133 136 rewriter.add_component("speech-box", speech_box_nocss); 134 137 rewriter.add_component("code-block", code_block_nocss);
+6 -6
src/post/view.rs
··· 1 - use super::{clock_icon, fetch::fetch_post, format_date}; 1 + use super::{clock_icon, format_date}; 2 2 use crate::{ 3 3 D, W, 4 4 error::Result, 5 - post::render_post, 5 + post, 6 6 template::{footer, header_extra, navbar}, 7 7 }; 8 8 use maud::{PreEscaped, html}; 9 9 use poem::{ 10 - IntoResponse, Response, handler, 10 + IntoResponse as _, Response, handler, 11 11 web::{Data, Path}, 12 12 }; 13 13 14 14 #[handler] 15 - pub fn view_post(Path(slug): Path<String>, Data(conn): D<&W>) -> Result<Response> { 16 - let post = fetch_post(slug, conn)?; 17 - let contents = render_post(&post.contents)?; 15 + pub fn view(Path(slug): Path<String>, Data(conn): D<&W>) -> Result<Response> { 16 + let post = post::fetch::one(slug, conn)?; 17 + let contents = post::render::render(&post.contents)?; 18 18 19 19 let datestring = format_date(post.creation_datetime); 20 20
+3 -3
src/template.rs
··· 141 141 } 142 142 143 143 pub enum SpeechEmotion { 144 + Happy, 144 145 Neutral, 145 - Worried, 146 146 Shocked, 147 - Happy, 147 + Worried, 148 148 } 149 149 150 150 pub enum SpeechCharacter { ··· 153 153 } 154 154 155 155 pub struct SpeechDetails { 156 - pub class: String, 157 156 pub alt: String, 157 + pub class: String, 158 158 pub src: String, 159 159 } 160 160 #[must_use]