1
0
Fork 0

too much stuff

This commit is contained in:
Adrian Hedqvist 2023-07-29 20:22:13 +02:00
parent cbfc505649
commit e0a3f35caf
21 changed files with 262 additions and 86 deletions

View file

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
</head>
<body>
<h1>404 Not Found</h1>
</body>
</html>

126
Cargo.lock generated
View file

@ -17,6 +17,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.0.2" version = "1.0.2"
@ -153,6 +164,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.2" version = "0.21.2"
@ -354,6 +371,25 @@ dependencies = [
"tracing-error", "tracing-error",
] ]
[[package]]
name = "config"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7"
dependencies = [
"async-trait",
"json5",
"lazy_static",
"nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.4" version = "0.8.4"
@ -471,6 +507,12 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "dlv-list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -708,6 +750,9 @@ name = "hashbrown"
version = "0.12.3" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
@ -942,6 +987,17 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1034,6 +1090,12 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.1" version = "0.7.1"
@ -1054,6 +1116,16 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -1166,6 +1238,16 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "ordered-multimap"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown 0.12.3",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1210,6 +1292,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.0" version = "2.3.0"
@ -1343,7 +1431,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06"
dependencies = [ dependencies = [
"base64", "base64 0.21.2",
"indexmap 1.9.3", "indexmap 1.9.3",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml",
@ -1515,6 +1603,27 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
"serde",
]
[[package]]
name = "rust-ini"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
@ -1891,6 +2000,15 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.7.6" version = "0.7.6"
@ -1953,7 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"base64", "base64 0.21.2",
"bitflags 2.3.3", "bitflags 2.3.3",
"bytes", "bytes",
"futures-core", "futures-core",
@ -2306,8 +2424,8 @@ dependencies = [
"cached", "cached",
"chrono", "chrono",
"color-eyre", "color-eyre",
"config",
"glob", "glob",
"lazy_static",
"opentelemetry", "opentelemetry",
"prometheus", "prometheus",
"pulldown-cmark", "pulldown-cmark",
@ -2318,7 +2436,7 @@ dependencies = [
"syntect", "syntect",
"tera", "tera",
"tokio", "tokio",
"toml", "toml 0.7.6",
"tower", "tower",
"tower-http", "tower-http",
"tracing", "tracing",

View file

@ -10,9 +10,8 @@ axum = { version = "0.6.12", features = ["http2", "original-uri"] }
cached = "0.44.0" cached = "0.44.0"
chrono = { version = "0.4.24", features = ["serde"] } chrono = { version = "0.4.24", features = ["serde"] }
color-eyre = "0.6.1" color-eyre = "0.6.1"
config = "0.13.3"
glob = "0.3.0" glob = "0.3.0"
# hyper = { version = "0.14.19", features = ["full"] }
lazy_static = "1.4.0"
opentelemetry = { version = "0.19.0", features = ["metrics"] } opentelemetry = { version = "0.19.0", features = ["metrics"] }
prometheus = { version = "0.13.3", features = ["process"] } prometheus = { version = "0.13.3", features = ["process"] }
pulldown-cmark = "0.9.2" pulldown-cmark = "0.9.2"

3
config.toml Normal file
View file

@ -0,0 +1,3 @@
base_url = "http://localhost:8080/"
bind_address = "0.0.0.0:8080"
logging = "info"

7
posts/draft-test.md Normal file
View file

@ -0,0 +1,7 @@
+++
title="draft test"
draft=true
date=2023-07-29T17:25:20+02:00
+++
wow look it's a hidden post because it's marked as a draft

View file

@ -13,6 +13,8 @@ here have a squid kid miku to test relative paths:
modified post test, see if docker skips build using modified post test, see if docker skips build using
its cache if only a post has changed. its cache if only a post has changed.
testing "smart" punctuation --- I don't know if I want it. 'it should' do some fancy stuff.
code hilighting test: code hilighting test:
```rs ```rs
@ -20,3 +22,11 @@ fn main() {
println!("Hello world!") println!("Hello world!")
} }
``` ```
uh oh, here comes a screenshot from a different post!
![dungeon screenshot](../dungeon/screenshot.png)
and here it is again, except it should 404!
![missing dungeon screenshot](../dungeon/screenshot.jpeg)

View file

@ -16,7 +16,7 @@ struct FeedContext<'a> {
#[instrument(skip(state))] #[instrument(skip(state))]
pub fn render_atom_feed(state: &AppState) -> Result<String> { pub fn render_atom_feed(state: &AppState) -> Result<String> {
let mut posts: Vec<_> = state.posts.values().filter(|p| p.is_published()).collect(); let mut posts: Vec<_> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect();
posts.sort_by_key(|p| &p.date); posts.sort_by_key(|p| &p.date);
posts.reverse(); posts.reverse();
@ -41,7 +41,7 @@ pub fn render_atom_tag_feed(tag: &Tag, state: &AppState) -> Result<String> {
let mut posts: Vec<_> = state let mut posts: Vec<_> = state
.posts .posts
.values() .values()
.filter(|p| p.is_published() && p.tags.contains(&tag.slug)) .filter(|p| !p.draft && p.is_published() && p.tags.contains(&tag.slug))
.collect(); .collect();
posts.sort_by_key(|p| &p.date); posts.sort_by_key(|p| &p.date);

View file

@ -7,14 +7,14 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use cached::once_cell::sync::Lazy;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use lazy_static::lazy_static;
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder}; use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
use std::sync::Arc; use std::sync::Arc;
use tower_http::services::ServeFile; use tower_http::services::ServeDir;
use tracing::{ use tracing::{
instrument, instrument,
log::{error, info}, log::{error, info, debug},
}; };
use crate::{AppState, WebsiteError}; use crate::{AppState, WebsiteError};
@ -22,16 +22,14 @@ use crate::{AppState, WebsiteError};
pub mod posts; pub mod posts;
pub mod tags; pub mod tags;
lazy_static! { pub static HIT_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| prometheus::register_int_counter_vec!(
pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!( opts!(
opts!( "http_requests_total",
"http_requests_total", "Total amount of http requests received"
"Total amount of http requests received" ),
), &["route", "method", "status"]
&["route", "method", "status"] )
) .unwrap());
.unwrap();
}
pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> { pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
Router::new() Router::new()
@ -44,13 +42,12 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.route_service( .route_service(
"/posts/:slug/*path", "/posts/:slug/*path",
tower_http::services::ServeDir::new("./").fallback(ServeFile::new("./404.html")), ServeDir::new("./"),
) )
.route_service( .route_service(
"/static/*path", "/static/*path",
tower_http::services::ServeDir::new("./").fallback(ServeFile::new("./404.html")), ServeDir::new("./"),
) )
.fallback_service(ServeFile::new("./404.html"))
} }
#[instrument(skip(state))] #[instrument(skip(state))]
@ -61,10 +58,11 @@ pub async fn index(
if should_return_304(&headers, Some(state.startup_time.into())) { if should_return_304(&headers, Some(state.startup_time.into())) {
return Ok(StatusCode::NOT_MODIFIED.into_response()); return Ok(StatusCode::NOT_MODIFIED.into_response());
} }
let ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("base_url", &state.base_url.to_string());
let res = state.tera.render("index.html", &ctx).map_err(|e| { let res = state.tera.render("index.html", &ctx).map_err(|e| {
error!("Failed rendering index: {}", e); error!("Failed rendering index: {}", e);
WebsiteError::NotFound WebsiteError::InternalError(e.into())
})?; })?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
@ -97,17 +95,17 @@ pub async fn not_found() -> impl IntoResponse {
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response { pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
let path = request.uri().path().to_string(); let path = request.uri().path().to_string();
let method = request.method().to_string(); let method = request.method().clone();
let response = next.run(request).await; let response = next.run(request).await;
if !response.status().is_client_error() { if !response.status().is_client_error() {
HIT_COUNTER HIT_COUNTER
.with_label_values(&[&path, &method, response.status().as_str()]) .with_label_values(&[&path, method.as_str(), response.status().as_str()])
.inc(); .inc();
} else if response.status() == StatusCode::NOT_FOUND { } else if response.status() == StatusCode::NOT_FOUND {
HIT_COUNTER HIT_COUNTER
.with_label_values(&["not found", &method, response.status().as_str()]) .with_label_values(&["not found", method.as_str(), response.status().as_str()])
.inc(); .inc();
} }
@ -116,16 +114,16 @@ pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Respon
fn should_return_304(headers: &HeaderMap, last_changed: Option<DateTime<FixedOffset>>) -> bool { fn should_return_304(headers: &HeaderMap, last_changed: Option<DateTime<FixedOffset>>) -> bool {
let Some(date) = last_changed else { let Some(date) = last_changed else {
info!("no last modified date"); debug!("no last modified date");
return false; return false;
}; };
let Some(since) = headers.get(header::IF_MODIFIED_SINCE) else { let Some(since) = headers.get(header::IF_MODIFIED_SINCE) else {
info!("no IF_MODIFIED_SINCE header"); debug!("no IF_MODIFIED_SINCE header");
return false; return false;
}; };
let Ok(parsed) = DateTime::<FixedOffset>::parse_from_rfc2822(since.to_str().unwrap()) else { let Ok(parsed) = DateTime::<FixedOffset>::parse_from_rfc2822(since.to_str().unwrap()) else {
info!("failed to parse IF_MODIFIED_SINCE header"); debug!("failed to parse IF_MODIFIED_SINCE header");
return false; return false;
}; };
@ -160,7 +158,8 @@ mod tests {
#[test] #[test]
fn render_index() { fn render_index() {
let tera = tera::Tera::new("templates/**/*").unwrap(); let tera = tera::Tera::new("templates/**/*").unwrap();
let ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("base_url", "http://localhost/");
let _res = tera.render("index.html", &ctx).unwrap(); let _res = tera.render("index.html", &ctx).unwrap();
} }

View file

@ -56,7 +56,7 @@ pub async fn index(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Response, WebsiteError> { ) -> Result<Response, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published()).collect(); let mut posts: Vec<&Post> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
@ -135,7 +135,7 @@ pub async fn feed(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Response, WebsiteError> { ) -> Result<Response, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published()).collect(); let mut posts: Vec<&Post> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
@ -203,6 +203,7 @@ mod tests {
]; ];
let page = PageContext { title: "Posts" }; let page = PageContext { title: "Posts" };
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("base_url", "http://localhost/");
ctx.insert("page", &page); ctx.insert("page", &page);
ctx.insert("posts", &posts); ctx.insert("posts", &posts);

View file

@ -57,7 +57,7 @@ pub async fn view(
let mut posts: Vec<&Post> = state let mut posts: Vec<&Post> = state
.posts .posts
.values() .values()
.filter(|p| p.is_published() && p.tags.contains(&tag)) .filter(|p| !p.draft && p.is_published() && p.tags.contains(&tag))
.collect(); .collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();

View file

@ -31,3 +31,28 @@ pub fn uri_with_path(uri: &Uri, path: &str) -> Uri {
.build() .build()
.unwrap(); .unwrap();
} }
#[cfg(test)]
mod tests {
use axum::http::Uri;
use crate::helpers::uri_with_path;
#[test]
fn uri_with_relative_path() {
let uri: Uri = "http://localhost/baba/".parse().unwrap();
assert_eq!(uri_with_path(&uri, "is/you").to_string(), "http://localhost/baba/is/you");
}
#[test]
fn uri_with_absolute_path() {
let uri: Uri = "http://localhost/baba/is/you".parse().unwrap();
assert_eq!(uri_with_path(&uri, "/keke/is/move").to_string(), "http://localhost/keke/is/move");
}
#[test]
fn uri_with_index_relative_path() {
let uri: Uri = "http://localhost/baba/is/you".parse().unwrap();
assert_eq!(uri_with_path(&uri, "happy").to_string(), "http://localhost/baba/is/happy");
}
}

View file

@ -11,6 +11,7 @@ use axum::{
use chrono::DateTime; use chrono::DateTime;
use color_eyre::eyre::{Error, Result}; use color_eyre::eyre::{Error, Result};
use config::Config;
use post::Post; use post::Post;
use tag::Tag; use tag::Tag;
@ -39,15 +40,20 @@ pub struct AppState {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let cfg = Config::builder()
.add_source(config::File::with_name("config.toml"))
.add_source(config::Environment::with_prefix("APP"))
.build()?;
color_eyre::install()?; color_eyre::install()?;
init_tracing(); init_tracing(&cfg);
info!("Starting server..."); info!("Starting server...");
let base_url: Uri = option_env!("SITE_BASE_URL") let base_url: Uri = cfg.get_string("base_url")?
.unwrap_or("http://localhost:8080")
.parse() .parse()
.unwrap(); .unwrap();
let tera = Tera::new("templates/**/*")?; let tera = Tera::new("templates/**/*")?;
let mut state = AppState { let mut state = AppState {
startup_time: chrono::offset::Utc::now(), startup_time: chrono::offset::Utc::now(),
@ -55,6 +61,7 @@ async fn main() -> Result<()> {
tera, tera,
..Default::default() ..Default::default()
}; };
let posts = post::load_all(&state).await?; let posts = post::load_all(&state).await?;
let tags = tag::get_tags(posts.values()); let tags = tag::get_tags(posts.values());
state.posts = posts; state.posts = posts;
@ -73,17 +80,23 @@ async fn main() -> Result<()> {
info!("Now listening at {}", state.base_url); info!("Now listening at {}", state.base_url);
axum::Server::bind(&"0.0.0.0:8080".parse().unwrap()) axum::Server::bind(&cfg.get_string("bind_address")?.parse().unwrap())
.serve(app.into_make_service()) .serve(app.into_make_service())
.await?; .await?;
Ok(()) Ok(())
} }
fn init_tracing() { fn init_tracing(cfg: &Config) {
let filter = EnvFilter::builder() let filter = if let Ok(filter) = cfg.get_string("logging") {
.with_default_directive("into".parse().unwrap()) EnvFilter::builder()
.from_env_lossy(); .with_default_directive("info".parse().unwrap())
.parse_lossy(filter)
} else {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.from_env_lossy()
};
tracing_subscriber::registry() tracing_subscriber::registry()
.with(filter) .with(filter)

View file

@ -1,14 +1,26 @@
use crate::helpers; use crate::helpers;
use crate::hilighting; use crate::hilighting;
use axum::http::Uri; use axum::http::Uri;
use cached::once_cell::sync::Lazy;
use pulldown_cmark::Event; use pulldown_cmark::Event;
use pulldown_cmark::Tag; use pulldown_cmark::Tag;
use pulldown_cmark::{Options, Parser}; use pulldown_cmark::{Options, Parser};
use regex::Regex;
static STARTS_WITH_SCHEMA_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\w+:").unwrap());
static EMAIL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^.+?@\w+(\.\w+)*$").unwrap());
pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String { pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String {
let options = Options::all(); let mut opt = Options::empty();
opt.insert(Options::ENABLE_FOOTNOTES);
opt.insert(Options::ENABLE_HEADING_ATTRIBUTES);
opt.insert(Options::ENABLE_STRIKETHROUGH);
opt.insert(Options::ENABLE_TABLES);
opt.insert(Options::ENABLE_TASKLISTS);
opt.insert(Options::ENABLE_SMART_PUNCTUATION);
let mut content_html = String::new(); let mut content_html = String::new();
let parser = Parser::new_ext(markdown, options); let parser = Parser::new_ext(markdown, opt);
let mut code_block = false; let mut code_block = false;
let mut code_lang = None; let mut code_lang = None;
@ -25,7 +37,7 @@ pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String
} }
Event::Start(Tag::Link(t, mut link, title)) => { Event::Start(Tag::Link(t, mut link, title)) => {
if let Some(uri) = base_uri { if let Some(uri) = base_uri {
if !link.contains("://") && !link.contains('@') { if !link.starts_with('#') && !STARTS_WITH_SCHEMA_RE.is_match(&link) && !EMAIL_RE.is_match(&link) {
// convert relative URIs to absolute URIs // convert relative URIs to absolute URIs
link = helpers::uri_with_path(uri, &link).to_string().into(); link = helpers::uri_with_path(uri, &link).to_string().into();
} }

View file

@ -1,10 +1,9 @@
use std::{collections::HashMap, path::Path}; use std::{collections::HashMap, path::Path};
use cached::once_cell::sync::Lazy;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use glob::glob; use glob::glob;
use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use tokio::fs; use tokio::fs;
@ -16,6 +15,11 @@ use tracing::{
use crate::{helpers, markdown, AppState, WebsiteError}; use crate::{helpers, markdown, AppState, WebsiteError};
static FRONTMATTER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
)
.unwrap());
#[derive(Deserialize, Debug, Default)] #[derive(Deserialize, Debug, Default)]
pub struct TomlFrontMatter { pub struct TomlFrontMatter {
pub title: String, pub title: String,
@ -29,6 +33,7 @@ pub struct TomlFrontMatter {
#[derive(Serialize, Clone, Debug, Default)] #[derive(Serialize, Clone, Debug, Default)]
pub struct Post { pub struct Post {
pub title: String, pub title: String,
pub draft: bool,
pub date: Option<DateTime<FixedOffset>>, pub date: Option<DateTime<FixedOffset>>,
pub updated: Option<DateTime<FixedOffset>>, pub updated: Option<DateTime<FixedOffset>>,
pub aliases: Vec<String>, pub aliases: Vec<String>,
@ -45,6 +50,7 @@ impl Post {
slug, slug,
content, content,
title: fm.title, title: fm.title,
draft: fm.draft.unwrap_or(false),
date: fm date: fm
.date .date
.map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")), .map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")),
@ -124,13 +130,6 @@ pub async fn load_post(state: &AppState, slug: &str) -> color_eyre::eyre::Result
fn parse_frontmatter( fn parse_frontmatter(
src: String, src: String,
) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> { ) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> {
lazy_static! {
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
)
.unwrap();
};
Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) { Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) {
( (
Some(toml::from_str(captures.get(1).unwrap().as_str())?), Some(toml::from_str(captures.get(1).unwrap().as_str())?),
@ -162,10 +161,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn render_all_posts() { async fn render_all_posts() {
let mut state = AppState { let mut state = AppState {
base_url: "localhost:8180".parse().unwrap(), base_url: "http://localhost:8180".parse().unwrap(),
tera: Tera::new("templates/**/*").unwrap(), tera: Tera::new("templates/**/*").unwrap(),
..Default::default() ..Default::default()
}; };
state.posts = super::load_all(&state).await.unwrap(); state.posts = super::load_all(&state).await.unwrap();
for post in state.posts.values() { for post in state.posts.values() {
super::render_post(&state, post).await.unwrap(); super::render_post(&state, post).await.unwrap();

View file

@ -2,16 +2,10 @@
<html lang="en"> <html lang="en">
{% include "partials/head.html" %} {% include "partials/head.html" %}
<body> <body>
<header> {% include "partials/header.html" -%}
{% include "partials/header.html" -%}
</header>
<hr>
<main> <main>
{% block main %}{% endblock main -%} {% block main %}{% endblock main -%}
</main> </main>
<hr> {% include "partials/footer.html" -%}
<footer>
{% include "partials/footer.html" -%}
</footer>
</body> </body>
</html> </html>

View file

@ -1 +1,4 @@
<p>footer stuff goes here</p> <footer>
<hr style="border-style: dashed;">
<p>footer stuff goes here</p>
</footer>

View file

@ -1,3 +1,5 @@
<nav> <header>
<img src="{{base_url | safe}}static/avatar.png" class="avatar"/> <a href="{{base_url | safe}}">tollyx</a> - <a href="{{base_url | safe}}posts/">posts</a> <nav>
</nav> <img src="{{base_url | safe}}static/avatar.png" class="avatar"/> <a href="{{base_url | safe}}">tollyx</a> - <a href="{{base_url | safe}}posts/">posts</a>
</nav>
</header>

View file

@ -6,9 +6,11 @@
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>
{% endif -%} {% endif -%}
{% if page.date -%} {% if page.date -%}
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time> <small>
{%- if page.draft %}Draft {% endif -%}
Published <time datetime="{{ page.date }}">{{ page.date | date(format="%F %R%:::z") }}</time>
{%- if page.updated -%} {%- if page.updated -%}
, Updated <time datetime="{{ page.updated }}">{{ page.updated | date(format="%Y-%m-%d %H:%M") }}</time> , Updated <time datetime="{{ page.updated }}">{{ page.updated | date(format="%F %R%:::z") }}</time>
{%- endif -%} {%- endif -%}
</small> </small>
{%- endif %} {%- endif %}

View file

@ -5,7 +5,7 @@
<ul> <ul>
{% for post in posts -%} {% for post in posts -%}
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{post.absolute_path | safe}}">{% if post.date -%} <li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{post.absolute_path | safe}}">{% if post.date -%}
<time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}} <time datetime="{{ post.date }}">{{ post.date | date(format="%F") }}</time> &ndash; {{ post.title -}}
{% else -%} {% else -%}
{{ post.title -}} {{ post.title -}}
{% endif -%} {% endif -%}

View file

@ -8,7 +8,7 @@
<ul> <ul>
{% for post in posts -%} {% for post in posts -%}
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{post.absolute_path | safe}}">{% if post.date -%} <li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{post.absolute_path | safe}}">{% if post.date -%}
<time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}} <time datetime="{{ post.date }}">{{ post.date | date(format="%F") }}</time> - {{ post.title -}}
{% else -%} {% else -%}
{{ post.title -}} {{ post.title -}}
{% endif -%} {% endif -%}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main -%} {% block main -%}
<h1>Tags</h1> <h2>Tags</h2>
<ul> <ul>
{% for tag in tags -%} {% for tag in tags -%}
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{tag.absolute_path | safe}}">#{{ tag.slug }}</a></li> <li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{tag.absolute_path | safe}}">#{{ tag.slug }}</a></li>