improving the experience brick by brick
This commit is contained in:
parent
ff2eae0ae1
commit
589de5b9da
8 changed files with 45 additions and 92 deletions
|
@ -173,10 +173,9 @@ async fn rss(
|
|||
|
||||
let posts = posts
|
||||
.get_all_posts(|metadata, _| {
|
||||
!query
|
||||
query
|
||||
.tag
|
||||
.as_ref()
|
||||
.is_some_and(|tag| !metadata.tags.contains(tag))
|
||||
.as_ref().is_none_or(|tag| metadata.tags.contains(tag))
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
|
12
src/error.rs
12
src/error.rs
|
@ -4,6 +4,7 @@ use askama_axum::Template;
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
|
@ -76,17 +77,14 @@ struct ErrorTemplate {
|
|||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let error = self.to_string();
|
||||
error!("error while handling request: {error}");
|
||||
|
||||
let status_code = match &self {
|
||||
AppError::PostError(PostError::NotFound(_)) => StatusCode::NOT_FOUND,
|
||||
AppError::RssDisabled => StatusCode::FORBIDDEN,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(
|
||||
status_code,
|
||||
ErrorTemplate {
|
||||
error: self.to_string(),
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
(status_code, ErrorTemplate { error }).into_response()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct HashArcStore<T, Lookup>
|
||||
where
|
||||
Lookup: Hash,
|
||||
{
|
||||
inner: Option<Arc<T>>,
|
||||
hash: Option<u64>,
|
||||
_phantom: PhantomData<Lookup>,
|
||||
}
|
||||
|
||||
impl<T, Lookup> HashArcStore<T, Lookup>
|
||||
where
|
||||
Lookup: Hash,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: None,
|
||||
hash: None,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_init(&mut self, key: &Lookup, init: impl Fn(&Lookup) -> Arc<T>) -> Arc<T> {
|
||||
let mut h = DefaultHasher::new();
|
||||
key.hash(&mut h);
|
||||
let hash = h.finish();
|
||||
if !self.hash.is_some_and(|inner_hash| inner_hash == hash) {
|
||||
self.inner = Some(init(key));
|
||||
self.hash = Some(hash);
|
||||
}
|
||||
// safety: please.
|
||||
unsafe { self.inner.as_ref().unwrap_unchecked().clone() }
|
||||
}
|
||||
}
|
20
src/main.rs
20
src/main.rs
|
@ -3,7 +3,6 @@
|
|||
mod app;
|
||||
mod config;
|
||||
mod error;
|
||||
mod hash_arc_store;
|
||||
mod helpers;
|
||||
mod markdown_render;
|
||||
mod platform;
|
||||
|
@ -77,25 +76,19 @@ async fn main() -> eyre::Result<()> {
|
|||
|
||||
let reg = Arc::new(RwLock::new(reg));
|
||||
|
||||
let watcher_token = cancellation_token.child_token();
|
||||
|
||||
let posts = Arc::new(MarkdownPosts::new(Arc::clone(&config)).await?);
|
||||
let state = AppState {
|
||||
config: Arc::clone(&config),
|
||||
posts: Arc::clone(&posts),
|
||||
reg: Arc::clone(®),
|
||||
};
|
||||
|
||||
debug!("setting up watcher");
|
||||
let watcher_token = cancellation_token.child_token();
|
||||
tasks.spawn(
|
||||
watch_templates(
|
||||
config.dirs.custom_templates.clone(),
|
||||
watcher_token.clone(),
|
||||
reg,
|
||||
reg.clone(),
|
||||
)
|
||||
.instrument(info_span!("custom_template_watcher")),
|
||||
);
|
||||
|
||||
let posts = Arc::new(MarkdownPosts::new(Arc::clone(&config)).await?);
|
||||
|
||||
if config.cache.enable && config.cache.cleanup {
|
||||
if let Some(millis) = config.cache.cleanup_interval {
|
||||
let posts = Arc::clone(&posts);
|
||||
|
@ -117,6 +110,11 @@ async fn main() -> eyre::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
let state = AppState {
|
||||
config: Arc::clone(&config),
|
||||
posts: Arc::clone(&posts),
|
||||
reg: Arc::clone(®),
|
||||
};
|
||||
let app = app::new(&config).with_state(state.clone());
|
||||
|
||||
let listener = TcpListener::bind(socket_addr)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::sync::{Arc, OnceLock, RwLock};
|
||||
|
||||
use color_eyre::eyre::{self, Context};
|
||||
use comrak::adapters::SyntaxHighlighterAdapter;
|
||||
use comrak::markdown_to_html_with_plugins;
|
||||
use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder};
|
||||
use comrak::ComrakOptions;
|
||||
|
@ -7,32 +7,26 @@ use comrak::RenderPlugins;
|
|||
use syntect::highlighting::ThemeSet;
|
||||
|
||||
use crate::config::RenderConfig;
|
||||
use crate::hash_arc_store::HashArcStore;
|
||||
|
||||
fn syntect_adapter(config: &RenderConfig) -> Arc<SyntectAdapter> {
|
||||
static STATE: OnceLock<RwLock<HashArcStore<SyntectAdapter, RenderConfig>>> = OnceLock::new();
|
||||
let lock = STATE.get_or_init(|| RwLock::new(HashArcStore::new()));
|
||||
let mut guard = lock.write().unwrap();
|
||||
guard.get_or_init(config, build_syntect)
|
||||
}
|
||||
|
||||
fn build_syntect(config: &RenderConfig) -> Arc<SyntectAdapter> {
|
||||
pub fn build_syntect(config: &RenderConfig) -> eyre::Result<SyntectAdapter> {
|
||||
let mut theme_set = if config.syntect.load_defaults {
|
||||
ThemeSet::load_defaults()
|
||||
} else {
|
||||
ThemeSet::new()
|
||||
};
|
||||
if let Some(path) = config.syntect.themes_dir.as_ref() {
|
||||
theme_set.add_from_folder(path).unwrap();
|
||||
theme_set
|
||||
.add_from_folder(path)
|
||||
.with_context(|| format!("failed to add themes from {path:?}"))?;
|
||||
}
|
||||
let mut builder = SyntectAdapterBuilder::new().theme_set(theme_set);
|
||||
if let Some(theme) = config.syntect.theme.as_ref() {
|
||||
builder = builder.theme(theme);
|
||||
}
|
||||
Arc::new(builder.build())
|
||||
Ok(builder.build())
|
||||
}
|
||||
|
||||
pub fn render(markdown: &str, config: &RenderConfig) -> String {
|
||||
pub fn render(markdown: &str, syntect: Option<&dyn SyntaxHighlighterAdapter>) -> String {
|
||||
let mut options = ComrakOptions::default();
|
||||
options.extension.table = true;
|
||||
options.extension.autolink = true;
|
||||
|
@ -43,8 +37,7 @@ pub fn render(markdown: &str, config: &RenderConfig) -> String {
|
|||
options.extension.header_ids = Some(String::new());
|
||||
|
||||
let mut render_plugins = RenderPlugins::default();
|
||||
let syntect = syntect_adapter(config);
|
||||
render_plugins.codefence_syntax_highlighter = Some(syntect.as_ref());
|
||||
render_plugins.codefence_syntax_highlighter = syntect;
|
||||
|
||||
let plugins = comrak::PluginsBuilder::default()
|
||||
.render(render_plugins)
|
||||
|
|
|
@ -9,6 +9,7 @@ use std::time::SystemTime;
|
|||
use axum::http::HeaderValue;
|
||||
use chrono::{DateTime, Utc};
|
||||
use color_eyre::eyre::{self, Context};
|
||||
use comrak::plugins::syntect::SyntectAdapter;
|
||||
use fronma::parser::{parse, ParsedData};
|
||||
use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
|
@ -16,7 +17,7 @@ use tokio::io::AsyncReadExt;
|
|||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::markdown_render::render;
|
||||
use crate::markdown_render::{build_syntect, render};
|
||||
use crate::post::cache::{load_cache, Cache, CACHE_VERSION};
|
||||
use crate::post::{PostError, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||
use crate::systemtime_as_secs::as_secs;
|
||||
|
@ -62,6 +63,7 @@ where
|
|||
{
|
||||
cache: Option<Cache>,
|
||||
config: C,
|
||||
syntect: SyntectAdapter,
|
||||
}
|
||||
|
||||
impl<C> MarkdownPosts<C>
|
||||
|
@ -69,7 +71,10 @@ where
|
|||
C: Deref<Target = Config>,
|
||||
{
|
||||
pub async fn new(config: C) -> eyre::Result<MarkdownPosts<C>> {
|
||||
if config.cache.enable {
|
||||
let syntect =
|
||||
build_syntect(&config.render).context("failed to create syntax highlighting engine")?;
|
||||
|
||||
let cache = if config.cache.enable {
|
||||
if config.cache.persistence && tokio::fs::try_exists(&config.cache.file).await? {
|
||||
info!("loading cache from file");
|
||||
let mut cache = load_cache(&config).await.unwrap_or_else(|err| {
|
||||
|
@ -83,22 +88,19 @@ where
|
|||
cache = Default::default();
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
cache: Some(cache),
|
||||
config,
|
||||
})
|
||||
Some(cache)
|
||||
} else {
|
||||
Ok(Self {
|
||||
cache: Some(Default::default()),
|
||||
config,
|
||||
})
|
||||
Some(Default::default())
|
||||
}
|
||||
} else {
|
||||
Ok(Self {
|
||||
cache: None,
|
||||
config,
|
||||
})
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
cache,
|
||||
config,
|
||||
syntect,
|
||||
})
|
||||
}
|
||||
|
||||
async fn parse_and_render(
|
||||
|
@ -126,7 +128,7 @@ where
|
|||
let parsing = parsing_start.elapsed();
|
||||
|
||||
let before_render = Instant::now();
|
||||
let post = render(body, &self.config.render);
|
||||
let post = render(body, Some(&self.syntect));
|
||||
let rendering = before_render.elapsed();
|
||||
|
||||
if let Some(cache) = self.cache.as_ref() {
|
||||
|
|
|
@ -58,7 +58,7 @@ pub trait PostManager {
|
|||
tag: Option<&String>,
|
||||
) -> Result<Vec<PostMetadata>, PostError> {
|
||||
let mut posts = self
|
||||
.get_all_post_metadata(|metadata| !tag.is_some_and(|tag| !metadata.tags.contains(tag)))
|
||||
.get_all_post_metadata(|metadata| tag.is_none_or(|tag| metadata.tags.contains(tag)))
|
||||
.await?;
|
||||
// we still want some semblance of order if created_at is None so sort by mtime as well
|
||||
posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
pub struct RangedI128Visitor<const START: i128, const END: i128>;
|
||||
impl<'de, const START: i128, const END: i128> serde::de::Visitor<'de>
|
||||
impl<const START: i128, const END: i128> serde::de::Visitor<'_>
|
||||
for RangedI128Visitor<START, END>
|
||||
{
|
||||
type Value = i128;
|
||||
|
|
Loading…
Reference in a new issue