Restructure project

main
Pantonshire 4 years ago
parent 0c158652c9
commit 14726ca7d4

19
Cargo.lock generated

@ -158,8 +158,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blog_server"
version = "0.1.0"
name = "blog"
version = "0.2.0"
dependencies = [
"atom_syndication",
"axum",
@ -232,13 +232,6 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "css_gen"
version = "0.1.0"
dependencies = [
"syntect",
]
[[package]]
name = "darling"
version = "0.12.4"
@ -773,9 +766,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.5.1"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
@ -953,9 +946,9 @@ checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]]
name = "parking_lot"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",

@ -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())
}

@ -1,12 +1,7 @@
mod codeblock;
mod fs_watcher;
mod post;
mod posts_store;
mod render;
mod service;
mod template;
mod time;
mod uuid;
use std::{env, fs, path::PathBuf, sync::Arc, thread};
@ -14,8 +9,12 @@ use axum::Server;
use miette::{IntoDiagnostic, Context};
use tracing::info;
use codeblock::CodeBlockRenderer;
use posts_store::ConcurrentPostsStore;
use blog::{
codeblock::CodeBlockRenderer,
db::ConcurrentPostsStore,
uuid,
};
use render::Renderer;
#[derive(knuffel::Decode, Clone, Debug)]

@ -10,14 +10,15 @@ use chrono::DateTime;
use notify::DebouncedEvent;
use tracing::{info, warn, error};
use crate::{
use blog::{
codeblock::CodeBlockRenderer,
Config,
post::{ParseError, Post, PostId},
posts_store::ConcurrentPostsStore,
post::{parse as parse_post, ParseError, Post, PostId},
db::ConcurrentPostsStore,
time::unix_epoch,
};
use crate::Config;
pub struct Renderer {
config: Arc<Config>,
posts: ConcurrentPostsStore,
@ -206,7 +207,7 @@ impl Renderer {
drop(fd);
Post::parse(
parse_post(
&self.code_renderer,
*self.config.namespace_uuid,
target.id.clone(),

@ -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 {

@ -5,13 +5,15 @@ use axum::{
extract::Extension,
};
use super::response::Rss;
use crate::{
Config,
posts_store::ConcurrentPostsStore,
use blog::{
db::ConcurrentPostsStore,
time::unix_epoch,
};
use crate::Config;
use super::response::Rss;
pub async fn handle(
Extension(config): Extension<Arc<Config>>,
Extension(posts): Extension<ConcurrentPostsStore>,

@ -11,10 +11,10 @@ use tower::limit::ConcurrencyLimitLayer;
use tower_http::trace::TraceLayer;
use tracing::info;
use crate::{
Config,
posts_store::ConcurrentPostsStore
};
use blog::db::ConcurrentPostsStore;
use crate::Config;
use super::{
atom,
contact,

@ -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();

@ -3,7 +3,7 @@ use syntect::html::{ClassedHTMLGenerator, ClassStyle};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "cb_" };
pub const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "cb_" };
pub struct CodeBlockRenderer {
syntax_set: SyntaxSet,
@ -71,3 +71,9 @@ impl CodeBlockRenderer {
}
}
}
impl Default for CodeBlockRenderer {
fn default() -> Self {
Self::new()
}
}

@ -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,187 +1,73 @@
use std::{borrow, error, fmt, ops};
use std::{error, fmt};
use chrono::{DateTime, Utc};
use libshire::{strings::ShString22, uuid::{Uuid, UuidV5Error}};
use maud::{Markup, PreEscaped, html};
use maud::{html, PreEscaped};
use crate::codeblock::CodeBlockRenderer;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct PostId(ShString22);
use super::{id::PostId, Post};
impl PostId {
pub fn from_file_name(file_name: &str) -> Option<Self> {
const POST_FILE_EXTENSION: &str = ".kdl.md";
fn is_invalid_char(c: char) -> bool {
c == '/' || c == '\\' || c == '.'
}
let prefix = file_name
.strip_suffix(POST_FILE_EXTENSION)?;
if prefix.contains(is_invalid_char) {
return None;
}
Some(Self(ShString22::new_from_str(prefix)))
}
}
impl ops::Deref for PostId {
type Target = str;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl ops::DerefMut for PostId {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut *self.0
}
}
impl AsRef<str> for PostId {
fn as_ref(&self) -> &str {
self
}
}
impl AsMut<str> for PostId {
fn as_mut(&mut self) -> &mut str {
self
}
}
impl borrow::Borrow<str> for PostId {
fn borrow(&self) -> &str {
self
}
}
impl borrow::BorrowMut<str> for PostId {
fn borrow_mut(&mut self) -> &mut str {
self
}
}
impl fmt::Display for PostId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&**self, f)
}
pub fn parse(
code_renderer: &CodeBlockRenderer,
namespace: Uuid,
post_id: PostId,
file_name: &str,
created: DateTime<Utc>,
updated: DateTime<Utc>,
source: &str,
) -> Result<Post, ParseError>
{
MdPost::parse(file_name, source)
.and_then(|post| render_mdpost(
code_renderer,
namespace,
post_id,
created,
updated,
post
))
}
pub struct Post {
uuid: Uuid,
fn render_mdpost(
code_renderer: &CodeBlockRenderer,
namespace: Uuid,
id: PostId,
title: String,
subtitle: Option<String>,
author: String,
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
}
mdpost: MdPost,
) -> Result<Post, ParseError>
{
use pulldown_cmark::{Options, Parser, html::push_html};
pub fn parse(
code_renderer: &CodeBlockRenderer,
namespace: Uuid,
post_id: PostId,
file_name: &str,
created: DateTime<Utc>,
updated: DateTime<Utc>,
source: &str,
) -> Result<Self, ParseError>
{
MdPost::parse(file_name, source)
.and_then(|post| Self::from_mdpost(
code_renderer,
namespace,
post_id,
created,
updated,
post
))
}
fn from_mdpost(
code_renderer: &CodeBlockRenderer,
namespace: Uuid,
id: PostId,
created: DateTime<Utc>,
updated: DateTime<Utc>,
mdpost: MdPost,
) -> Result<Self, ParseError>
{
use pulldown_cmark::{Options, Parser, html::push_html};
const PARSER_OPTIONS: Options = Options::ENABLE_TABLES
.union(Options::ENABLE_FOOTNOTES)
.union(Options::ENABLE_STRIKETHROUGH);
let uuid = Uuid::new_v5(namespace, &*id)
.map_err(|err| match err {
UuidV5Error::NameTooLong(len) => ParseError::IdTooLong(len),
})?;
let mut parser = PostMdParser::new(
Parser::new_ext(&mdpost.markdown, PARSER_OPTIONS),
code_renderer
);
let mut html_buf = String::new();
push_html(&mut html_buf, parser.by_ref());
Ok(Self {
uuid,
id,
title: mdpost.title,
subtitle: mdpost.subtitle,
author: mdpost.author,
html: PreEscaped(html_buf),
tags: mdpost.tags,
created,
updated,
})
}
const PARSER_OPTIONS: Options = Options::ENABLE_TABLES
.union(Options::ENABLE_FOOTNOTES)
.union(Options::ENABLE_STRIKETHROUGH);
let uuid = Uuid::new_v5(namespace, &*id)
.map_err(|err| match err {
UuidV5Error::NameTooLong(len) => ParseError::IdTooLong(len),
})?;
let mut parser = PostMdParser::new(
Parser::new_ext(&mdpost.markdown, PARSER_OPTIONS),
code_renderer
);
let mut html_buf = String::new();
push_html(&mut html_buf, parser.by_ref());
Ok(Post {
uuid,
id,
title: mdpost.title,
subtitle: mdpost.subtitle,
author: mdpost.author,
html: PreEscaped(html_buf),
tags: mdpost.tags,
created,
updated,
})
}
/// Iterator struct which wraps another event iterator in order to render code blocks, collect the links
@ -283,7 +169,7 @@ struct MdPost {
markdown: String,
title: String,
subtitle: Option<String>,
author: String,
author: ShString22,
tags: Vec<ShString22>,
}
@ -303,7 +189,7 @@ impl MdPost {
markdown: md.to_owned(),
title: header.title,
subtitle: header.subtitle,
author: header.author,
author: header.author.into(),
tags: header.tags.into_iter().map(|tag| tag.tag.into()).collect(),
})
}

@ -1,7 +0,0 @@
[package]
name = "css_gen"
version = "0.1.0"
edition = "2021"
[dependencies]
syntect = "4"
Loading…
Cancel
Save