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 (code, headers, html).into_response()
77 } else {
78 response
79 }
80}
81
82pub fn htmlify<
83 I: 'static
84 + Send
85 + Sync
86 + Clone
87 + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
88>() -> impl 'static
89+ Send
90+ Sync
91+ Clone
92+ Layer<
93 I,
94 Service: 'static
95 + Send
96 + Sync
97 + Clone
98 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
99> {
100 middleware::from_fn(axum_htmlify)
101}
102
103pub fn catify<
104 I: 'static
105 + Send
106 + Sync
107 + Clone
108 + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
109>() -> impl 'static
110+ Send
111+ Sync
112+ Clone
113+ Layer<
114 I,
115 Service: 'static
116 + Send
117 + Sync
118 + Clone
119 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
120> {
121 middleware::from_fn(axum_catify)
122}
123
124pub fn htmlify_catify<
125 I: 'static
126 + Send
127 + Sync
128 + Clone
129 + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
130>() -> impl 'static
131+ Send
132+ Sync
133+ Clone
134+ Layer<
135 I,
136 Service: 'static
137 + Send
138 + Sync
139 + Clone
140 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
141> {
142 Stack::new(catify(), htmlify())
143}