You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
7.7 KiB
Rust

use std::{
borrow::Cow,
fmt::{self, Write},
};
use axum::{
body::{Bytes, Full},
http::{header::{self, HeaderValue}, StatusCode},
response::{IntoResponse, Response},
};
use maud::{html, Markup, Render, Escaper, DOCTYPE};
#[derive(Debug)]
pub(super) enum Error {
Internal,
PostNotFound,
StaticResourceNotFound,
RouteNotFound,
}
impl Error {
fn status_code(&self) -> StatusCode {
match self {
Error::Internal => StatusCode::INTERNAL_SERVER_ERROR,
Error::PostNotFound => StatusCode::NOT_FOUND,
Error::StaticResourceNotFound => StatusCode::NOT_FOUND,
Error::RouteNotFound => StatusCode::NOT_FOUND,
}
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let status_code = self.status_code();
// Create a string buffer containing the full error text, e.g. "404 Not Found".
let status_text = {
let status_code_str = status_code.as_str();
let reason = status_code.canonical_reason();
// Allocate a buffer with enough capacity to store the full error text.
let mut buf = String::with_capacity(
status_code_str.len() + reason.map(|reason| reason.len() + 1).unwrap_or(0));
// Push the numerical code string first, then a space, then the error reason string.
buf.push_str(status_code_str);
if let Some(reason) = reason {
buf.push(' ');
buf.push_str(reason);
}
buf
};
Html::new()
.with_status(status_code)
.with_body(html! {
p { (status_text) }
})
.with_title_owned(status_text)
.into_response()
}
}
pub(super) struct Html {
status: StatusCode,
title: Cow<'static, str>,
head: Option<Markup>,
body: Option<Markup>,
crawler_hints: CrawlerHints,
}
impl Html {
pub(super) fn new() -> Self {
Self {
status: StatusCode::OK,
title: Cow::Borrowed("untitled"),
head: None,
body: None,
crawler_hints: CrawlerHints::restrictive(),
}
}
pub(super) fn with_status(self, status: StatusCode) -> Self {
Self { status, ..self }
}
pub(super) fn with_title(self, title: Cow<'static, str>) -> Self {
Self { title, ..self }
}
pub(super) fn with_title_static(self, title: &'static str) -> Self {
self.with_title(Cow::Borrowed(title))
}
pub(super) fn with_title_owned(self, title: String) -> Self {
self.with_title(Cow::Owned(title))
}
pub(super) fn with_head(self, head: Markup) -> Self {
Self { head: Some(head), ..self }
}
pub(super) fn with_body(self, body: Markup) -> Self {
Self { body: Some(body), ..self }
}
pub(super) fn with_crawler_hints(self, crawler_hints: CrawlerHints) -> Self {
Self { crawler_hints, ..self }
}
pub(super) fn with_crawler_restrictive(self) -> Self {
self.with_crawler_hints(CrawlerHints::restrictive())
}
pub(super) fn with_crawler_permissive(self) -> Self {
self.with_crawler_hints(CrawlerHints::permissive())
}
}
impl Default for Html {
fn default() -> Self {
Self::new()
}
}
impl IntoResponse for Html {
fn into_response(self) -> Response {
let html_doc = html! {
(DOCTYPE)
html {
head {
meta charset="utf-8";
meta name="robots" content=(self.crawler_hints);
meta name="viewport" content="width=device-width, initial-scale=1";
title { (self.title) }
link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png";
link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png";
link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png";
link rel="manifest" href="/site.webmanifest";
@if let Some(head) = self.head {
(head)
}
}
body {
@if let Some(body) = self.body {
(body)
}
}
}
};
(self.status, axum::response::Html(html_doc.into_string()))
.into_response()
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct CrawlerHints {
index: bool,
follow: bool,
archive: bool,
snippet: bool,
image_index: bool,
}
impl CrawlerHints {
pub(super) const fn restrictive() -> Self {
Self {
index: false,
follow: false,
archive: false,
snippet: false,
image_index: false,
}
}
pub(super) const fn permissive() -> Self {
Self {
index: true,
follow: true,
archive: true,
snippet: true,
image_index: true,
}
}
pub(super) const fn with_index(self, index: bool) -> Self {
Self { index, ..self }
}
pub(super) const fn with_follow(self, follow: bool) -> Self {
Self { follow, ..self }
}
pub(super) const fn with_archive(self, archive: bool) -> Self {
Self { archive, ..self }
}
pub(super) const fn with_snippet(self, snippet: bool) -> Self {
Self { snippet, ..self }
}
pub(super) const fn with_image_index(self, image_index: bool) -> Self {
Self { image_index, ..self }
}
fn index_str(self) -> &'static str {
if self.index {
"index"
} else {
"noindex"
}
}
fn follow_str(self) -> &'static str {
if self.follow {
"follow"
} else {
"nofollow"
}
}
fn archive_strs(self) -> Option<[&'static str; 2]> {
if self.archive {
None
} else {
Some(["noarchive", "nocache"])
}
}
fn snippet_str(self) -> Option<&'static str> {
if self.snippet {
None
} else {
Some("nosnippet")
}
}
fn image_index_str(self) -> Option<&'static str> {
if self.image_index {
None
} else {
Some("noimageindex")
}
}
fn write_meta_list_to<W: Write>(self, mut buf: W) -> fmt::Result {
write!(buf, "{},{}", self.index_str(), self.follow_str())?;
if let Some([archive_str, cache_str]) = self.archive_strs() {
write!(buf, ",{},{}", archive_str, cache_str)?;
}
if let Some(snippet_str) = self.snippet_str() {
write!(buf, ",{}", snippet_str)?;
}
if let Some(image_index_str) = self.image_index_str() {
write!(buf, ",{}", image_index_str)?;
}
Ok(())
}
}
impl Render for CrawlerHints {
fn render_to(&self, buf: &mut String) {
let escaper = Escaper::new(buf);
let _result = self.write_meta_list_to(escaper);
}
}
pub(super) struct Rss<T>(pub T);
impl<T: Into<Full<Bytes>>> IntoResponse for Rss<T> {
fn into_response(self) -> Response {
let headers = [
(header::CONTENT_TYPE, HeaderValue::from_static("application/rss+xml")),
];
(headers, self.0.into())
.into_response()
}
}
pub(super) struct Atom<T>(pub T);
impl<T: Into<Full<Bytes>>> IntoResponse for Atom<T> {
fn into_response(self) -> Response {
let headers = [
(header::CONTENT_TYPE, HeaderValue::from_static("application/atom+xml")),
];
(headers, self.0.into())
.into_response()
}
}