Restructure project
parent
0c158652c9
commit
14726ca7d4
@ -1,6 +1,35 @@
|
||||
[workspace]
|
||||
[package]
|
||||
name = "blog"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
members = [
|
||||
"blog_server",
|
||||
"utils/css_gen"
|
||||
]
|
||||
[lib]
|
||||
name = "blog"
|
||||
path = "src/lib/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "blog_server"
|
||||
path = "src/bin/blog_server/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "css_gen"
|
||||
path = "src/bin/css_gen/main.rs"
|
||||
|
||||
[dependencies]
|
||||
libshire = { git = "https://github.com/pantonshire/libshire" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
axum = "0.5"
|
||||
tower = { version = "0.4", features = ["limit"] }
|
||||
tower-http = { version = "0.3", features = ["fs", "trace"] }
|
||||
mime = "0.3"
|
||||
maud = "0.23"
|
||||
atom_syndication = "0.11"
|
||||
rss = "2"
|
||||
knuffel = "2"
|
||||
pulldown-cmark = "0.9"
|
||||
syntect = "4"
|
||||
notify = "4"
|
||||
chrono = "0.4"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
miette = { version = "4", features = ["fancy"] }
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
[package]
|
||||
name = "blog_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# My own utilities library
|
||||
libshire = { git = "https://github.com/pantonshire/libshire" }
|
||||
# Async runtime for Axum
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
# Web server framework
|
||||
axum = "0.5"
|
||||
# Middleware for the web server
|
||||
tower = { version = "0.4", features = ["limit"] }
|
||||
tower-http = { version = "0.3", features = ["fs", "trace"] }
|
||||
# MIME type implementation
|
||||
mime = "0.3"
|
||||
# Compile-time HTTP templating
|
||||
maud = "0.23"
|
||||
# Serialisation for RSS and Atom
|
||||
atom_syndication = "0.11"
|
||||
rss = "2"
|
||||
# KDL parsing
|
||||
knuffel = "2"
|
||||
# CommonMark parsing
|
||||
pulldown-cmark = "0.9"
|
||||
# Syntax highlighting
|
||||
syntect = "4"
|
||||
# Filesystem event watcher
|
||||
notify = "4"
|
||||
# Time library
|
||||
chrono = "0.4"
|
||||
# Logging for observability
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
# Pretty errors
|
||||
miette = { version = "4", features = ["fancy"] }
|
||||
@ -1,82 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use atom_syndication as atom;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::Extension,
|
||||
};
|
||||
|
||||
use super::response::Atom;
|
||||
use crate::{
|
||||
Config,
|
||||
posts_store::ConcurrentPostsStore,
|
||||
time::unix_epoch,
|
||||
};
|
||||
|
||||
pub async fn handle(
|
||||
Extension(config): Extension<Arc<Config>>,
|
||||
Extension(posts): Extension<ConcurrentPostsStore>,
|
||||
) -> Atom<Bytes> {
|
||||
let (atom_entries, updated) = {
|
||||
let guard = posts.read().await;
|
||||
|
||||
let atom_entries = guard.iter_by_created()
|
||||
.take(config.atom.num_posts)
|
||||
.map(|post| {
|
||||
atom::EntryBuilder::default()
|
||||
.id(format!("urn:uuid:{}", post.uuid()))
|
||||
.title(post.title().to_owned())
|
||||
.updated(post.updated())
|
||||
.links(vec![
|
||||
atom::LinkBuilder::default()
|
||||
.href(format!(
|
||||
"{}://{}/articles/{}",
|
||||
config.self_ref.protocol,
|
||||
config.self_ref.domain,
|
||||
post.id()
|
||||
))
|
||||
.rel("alternate".to_owned())
|
||||
.mime_type(Some("text/html".to_owned()))
|
||||
.build()
|
||||
])
|
||||
.author(atom::PersonBuilder::default()
|
||||
.name(post.author().to_owned())
|
||||
.build())
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<atom::Entry>>();
|
||||
|
||||
let updated = guard.last_updated()
|
||||
.unwrap_or_else(unix_epoch);
|
||||
|
||||
(atom_entries, updated)
|
||||
};
|
||||
|
||||
Atom(atom::FeedBuilder::default()
|
||||
.id(format!("urn:uuid:{}", *config.namespace_uuid))
|
||||
.title(config.atom.title.clone())
|
||||
.updated(updated)
|
||||
.links(vec![
|
||||
atom::LinkBuilder::default()
|
||||
.href(format!(
|
||||
"{}://{}/atom.xml",
|
||||
config.self_ref.protocol,
|
||||
config.self_ref.domain
|
||||
))
|
||||
.rel("self".to_owned())
|
||||
.build(),
|
||||
atom::LinkBuilder::default()
|
||||
.href(format!(
|
||||
"{}://{}/articles/",
|
||||
config.self_ref.protocol,
|
||||
config.self_ref.domain
|
||||
))
|
||||
.rel("alternate".to_owned())
|
||||
.mime_type(Some("text/html".to_owned()))
|
||||
.build()
|
||||
])
|
||||
.entries(atom_entries)
|
||||
.build()
|
||||
.to_string()
|
||||
.into())
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use atom_syndication as atom;
|
||||
use axum::{body::Bytes, extract::Extension};
|
||||
|
||||
use blog::{db::ConcurrentPostsStore, time::unix_epoch};
|
||||
|
||||
use crate::Config;
|
||||
|
||||
use super::response::Atom;
|
||||
|
||||
pub async fn handle(
|
||||
Extension(config): Extension<Arc<Config>>,
|
||||
Extension(posts): Extension<ConcurrentPostsStore>,
|
||||
) -> Atom<Bytes> {
|
||||
let (atom_entries, updated) = {
|
||||
let guard = posts.read().await;
|
||||
|
||||
let atom_entries = guard
|
||||
.iter_by_created()
|
||||
.take(config.atom.num_posts)
|
||||
.map(|post| {
|
||||
atom::EntryBuilder::default()
|
||||
.id(format!("urn:uuid:{}", post.uuid()))
|
||||
.title(post.title().to_owned())
|
||||
.updated(post.updated())
|
||||
.links(vec![atom::LinkBuilder::default()
|
||||
.href(format!(
|
||||
"{}://{}/articles/{}",
|
||||
config.self_ref.protocol,
|
||||
config.self_ref.domain,
|
||||
post.id()
|
||||
))
|
||||
.rel("alternate".to_owned())
|
||||
.mime_type(Some("text/html".to_owned()))
|
||||
.build()])
|
||||
.author(
|
||||
atom::PersonBuilder::default()
|
||||
.name(post.author().to_owned())
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<atom::Entry>>();
|
||||
|
||||
let updated = guard.last_updated().unwrap_or_else(unix_epoch);
|
||||
|
||||
(atom_entries, updated)
|
||||
};
|
||||
|
||||
Atom(
|
||||
atom::FeedBuilder::default()
|
||||
.id(format!("urn:uuid:{}", *config.namespace_uuid))
|
||||
.title(config.atom.title.clone())
|
||||
.updated(updated)
|
||||
.links(vec![
|
||||
atom::LinkBuilder::default()
|
||||
.href(format!(
|
||||
"{}://{}/atom.xml",
|
||||
config.self_ref.protocol, config.self_ref.domain
|
||||
))
|
||||
.rel("self".to_owned())
|
||||
.build(),
|
||||
atom::LinkBuilder::default()
|
||||
.href(format!(
|
||||
"{}://{}/articles/",
|
||||
config.self_ref.protocol, config.self_ref.domain
|
||||
))
|
||||
.rel("alternate".to_owned())
|
||||
.mime_type(Some("text/html".to_owned()))
|
||||
.build(),
|
||||
])
|
||||
.entries(atom_entries)
|
||||
.build()
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
use maud::html;
|
||||
|
||||
use crate::template;
|
||||
|
||||
use super::response::Html;
|
||||
|
||||
pub async fn handle() -> Html {
|
||||
@ -1,10 +1,10 @@
|
||||
use axum::extract::Extension;
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
posts_store::ConcurrentPostsStore,
|
||||
template,
|
||||
};
|
||||
use blog::db::ConcurrentPostsStore;
|
||||
|
||||
use crate::template;
|
||||
|
||||
use super::response::Html;
|
||||
|
||||
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
||||
@ -1,10 +1,10 @@
|
||||
use axum::extract::{Extension, Path};
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
posts_store::ConcurrentPostsStore,
|
||||
template,
|
||||
};
|
||||
use blog::db::ConcurrentPostsStore;
|
||||
|
||||
use crate::template;
|
||||
|
||||
use super::response::{Error, Html};
|
||||
|
||||
pub async fn handle(
|
||||
@ -1,10 +1,10 @@
|
||||
use axum::extract::Extension;
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
posts_store::ConcurrentPostsStore,
|
||||
template,
|
||||
};
|
||||
use blog::db::ConcurrentPostsStore;
|
||||
|
||||
use crate::template;
|
||||
|
||||
use super::response::Html;
|
||||
|
||||
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
||||
@ -1,10 +1,11 @@
|
||||
use std::env;
|
||||
use std::process;
|
||||
use std::{env, process};
|
||||
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
|
||||
use syntect::{
|
||||
highlighting::ThemeSet,
|
||||
html::css_for_theme_with_class_style,
|
||||
};
|
||||
|
||||
const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "cb_" };
|
||||
use blog::codeblock::CLASS_STYLE;
|
||||
|
||||
fn main() {
|
||||
let theme_set = ThemeSet::load_defaults();
|
||||
@ -0,0 +1,5 @@
|
||||
pub mod codeblock;
|
||||
pub mod post;
|
||||
pub mod db;
|
||||
pub mod time;
|
||||
pub mod uuid;
|
||||
@ -0,0 +1,88 @@
|
||||
use std::{borrow, fmt, ops};
|
||||
|
||||
use libshire::strings::ShString22;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub struct PostId(ShString22);
|
||||
|
||||
impl PostId {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn from_file_name(file_name: &str) -> Option<Self> {
|
||||
fn is_invalid_char(c: char) -> bool {
|
||||
c == '/' || c == '\\' || c == '.'
|
||||
}
|
||||
|
||||
let prefix = file_name
|
||||
.strip_suffix(super::POST_FILE_EXTENSION)?;
|
||||
|
||||
if prefix.contains(is_invalid_char) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self(ShString22::new_from_str(prefix)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> ShString22 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn as_inner(&self) -> &ShString22 {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for PostId {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for PostId {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut *self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PostId {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<str> for PostId {
|
||||
#[inline]
|
||||
fn as_mut(&mut self) -> &mut str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl borrow::Borrow<str> for PostId {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl borrow::BorrowMut<str> for PostId {
|
||||
#[inline]
|
||||
fn borrow_mut(&mut self) -> &mut str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PostId {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
mod id;
|
||||
mod parse;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use libshire::{strings::ShString22, uuid::Uuid};
|
||||
use maud::{Markup, PreEscaped};
|
||||
|
||||
pub use id::PostId;
|
||||
pub use parse::{parse, ParseError};
|
||||
|
||||
const POST_FILE_EXTENSION: &str = ".kdl.md";
|
||||
|
||||
pub struct Post {
|
||||
uuid: Uuid,
|
||||
id: PostId,
|
||||
title: String,
|
||||
subtitle: Option<String>,
|
||||
author: ShString22,
|
||||
html: Markup,
|
||||
tags: Vec<ShString22>,
|
||||
created: DateTime<Utc>,
|
||||
updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
pub fn uuid(&self) -> Uuid {
|
||||
self.uuid
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &PostId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn subtitle(&self) -> Option<&str> {
|
||||
self.subtitle.as_deref()
|
||||
}
|
||||
|
||||
pub fn author(&self) -> &str {
|
||||
&self.author
|
||||
}
|
||||
|
||||
pub fn html(&self) -> PreEscaped<&str> {
|
||||
PreEscaped(&self.html.0)
|
||||
}
|
||||
|
||||
pub fn tags(&self) -> &[ShString22] {
|
||||
&self.tags
|
||||
}
|
||||
|
||||
pub fn created(&self) -> DateTime<Utc> {
|
||||
self.created
|
||||
}
|
||||
|
||||
pub fn updated(&self) -> DateTime<Utc> {
|
||||
self.updated
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "css_gen"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
syntect = "4"
|
||||
Loading…
Reference in New Issue