1use std::convert::Infallible;
2
3use axum::{
4 Extension,
5 extract::{Request, rejection::ExtensionRejection},
6 http::HeaderMap,
7 middleware::{self, Next},
8 response::{IntoResponse, Response},
9};
10use maud::DOCTYPE;
11use tower::{Layer, Service, layer::util::Stack};
12
13#[derive(Clone)]
14pub struct AcceptHtml;
15
16async fn axum_htmlify(headers: HeaderMap, mut request: Request, next: Next) -> Response {
17 if let Some(accept) = headers.get("accept")
18 && let Ok(accept) = accept.to_str()
19 && accept.split(',').any(|s| s == "text/html")
20 {
21 request.extensions_mut().insert(AcceptHtml);
22 }
23 next.run(request).await
24}
25
26#[derive(Clone)]
27pub struct NoCats;
28
29async fn axum_catify(
30 extension: Result<Extension<AcceptHtml>, ExtensionRejection>,
31 request: Request,
32 next: Next,
33) -> Response {
34 let html = extension.is_ok();
35 let response = next.run(request).await;
36 if html
37 && response
38 .headers()
39 .get("content-type")
40 .is_none_or(|ty| ty == "text/plain; charset=utf-8")
41 && response.extensions().get::<NoCats>().is_none()
42 {
43 let code = response.status();
44 let number = code.as_u16();
45 let src = format!("https://http.cat/{number}");
46 let mut headers = response.headers().clone();
47 headers.remove("content-type");
48 let html = maud::html! {
49 (DOCTYPE)
50 html {
51 head {
52 title { (code) }
53 meta name="viewport" content="width=device-width, initial-scale=1";
54 meta charset="utf-8";
55 style {
56 r#"
57html, body {
58 margin: 0;
59 background: black;
60 display: flex;
61 width: 100vw;
62 height: 100vh;
63}
64
65img {
66 margin: auto;
67}
68"#
69 }
70 }
71 body {
72 img src=(src);
73 }
74 }
75 };
76 let status = response.status();
77 (status, headers, html).into_response()
78 } else {
79 response
80 }
81}
82
83pub fn htmlify<
84 I: 'static
85 + Send
86 + Sync
87 + Clone
88 + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
89>() -> impl 'static
90+ Send
91+ Sync
92+ Clone
93+ Layer<
94 I,
95 Service: 'static
96 + Send
97 + Sync
98 + Clone
99 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
100> {
101 middleware::from_fn(axum_htmlify)
102}
103
104pub fn catify<
105 I: 'static
106 + Send
107 + Sync
108 + Clone
109 + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
110>() -> impl 'static
111+ Send
112+ Sync
113+ Clone
114+ Layer<
115 I,
116 Service: 'static
117 + Send
118 + Sync
119 + Clone
120 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
121> {
122 middleware::from_fn(axum_catify)
123}
124
125pub fn htmlify_catify<
126 I: 'static
127 + Send
128 + Sync
129 + Clone
130 + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
131>() -> impl 'static
132+ Send
133+ Sync
134+ Clone
135+ Layer<
136 I,
137 Service: 'static
138 + Send
139 + Sync
140 + Clone
141 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
142> {
143 Stack::new(catify(), htmlify())
144}