trillium_html_rewriter/lib.rs
1#![forbid(unsafe_code)]
2#![deny(
3 clippy::dbg_macro,
4 missing_copy_implementations,
5 rustdoc::missing_crate_level_docs,
6 missing_debug_implementations,
7 missing_docs,
8 nonstandard_style,
9 unused_qualifications
10)]
11#![doc = include_str!("../README.md")]
12
13use lol_async::rewrite;
14pub use lol_async::{Settings, html};
15use mime::Mime;
16use std::{
17 fmt::{self, Debug, Formatter},
18 future::{Future, ready},
19 pin::Pin,
20 str::FromStr,
21 sync::Arc,
22};
23use trillium::{
24 Body, Conn, Handler,
25 KnownHeaderName::{ContentLength, ContentType},
26};
27
28/// A trillium [`Handler`] that rewrites HTML response bodies with
29/// [`lol-html`](https://docs.rs/lol-html), using [`lol-async`](https://docs.rs/lol-async).
30///
31/// It wraps the response produced by other handlers: in [`before_send`](Handler::before_send) it
32/// inspects the outgoing `Content-Type` and, if the mime subtype is `html` (e.g. `text/html`),
33/// replaces the response body with a streaming rewrite driven by the [`Settings`] returned from
34/// the settings function passed to [`new`](HtmlRewriter::new),
35/// [`new_with_conn`](HtmlRewriter::new_with_conn), or [`new_async`](HtmlRewriter::new_async).
36/// Responses with any other content type (or none) are passed through unchanged.
37pub struct HtmlRewriter {
38 settings: Arc<dyn ErasedSettingsFn>,
39}
40
41/// An async function from [`&Conn`](Conn) to [`Settings`], as accepted by
42/// [`HtmlRewriter::new_async`].
43///
44/// This trait is implemented for any `async` closure or `async fn` that takes a `&Conn` and
45/// returns `Settings<'static, 'static>`, as well as for plain closures that return such a future.
46/// The lifetime parameter allows the future to borrow the `Conn` across `.await` points; because
47/// of that, the bound in [`HtmlRewriter::new_async`] is the higher-ranked
48/// `F: for<'a> SettingsFn<'a>`.
49///
50/// You should not need to implement or name this trait directly — write one of the closure forms
51/// documented on [`HtmlRewriter::new_async`].
52#[diagnostic::on_unimplemented(
53 message = "`{Self}` is not an async settings builder",
54 label = "expected an async function from `&Conn` to `Settings<'static, 'static>`",
55 note = "if the settings don't require awaiting anything, use `HtmlRewriter::new` or \
56 `HtmlRewriter::new_with_conn` instead of `HtmlRewriter::new_async`",
57 note = "write an async closure with an annotated parameter: `async |conn: &Conn| {{ .. }}`, \
58 not `|conn| async move {{ .. }}` — and the `&Conn` annotation is required for \
59 inference",
60 note = "async closures that capture state don't implement `Fn`; to use captured state, write \
61 a plain closure returning an async block that owns its data: `move |conn: &Conn| {{ \
62 let data = data.clone(); async move {{ .. }} }}`",
63 note = "the returned future must be `Send`"
64)]
65pub trait SettingsFn<'a>: Send + Sync + 'static {
66 /// The future returned by [`call`](SettingsFn::call).
67 type Fut: Future<Output = Settings<'static, 'static>> + Send + 'a;
68
69 /// Build the [`Settings`] that will rewrite the response on this [`Conn`].
70 fn call(&self, conn: &'a Conn) -> Self::Fut;
71}
72
73impl<'a, F, Fut> SettingsFn<'a> for F
74where
75 F: Fn(&'a Conn) -> Fut + Send + Sync + 'static,
76 Fut: Future<Output = Settings<'static, 'static>> + Send + 'a,
77{
78 type Fut = Fut;
79
80 fn call(&self, conn: &'a Conn) -> Fut {
81 self(conn)
82 }
83}
84
85type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
86
87/// Object-safe form of [`SettingsFn`] for storage in [`HtmlRewriter`].
88trait ErasedSettingsFn: Send + Sync {
89 fn call<'a>(&'a self, conn: &'a Conn) -> BoxFuture<'a, Settings<'static, 'static>>;
90}
91
92impl<F> ErasedSettingsFn for F
93where
94 F: for<'a> SettingsFn<'a>,
95{
96 fn call<'a>(&'a self, conn: &'a Conn) -> BoxFuture<'a, Settings<'static, 'static>> {
97 Box::pin(SettingsFn::call(self, conn))
98 }
99}
100
101impl Debug for HtmlRewriter {
102 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
103 f.debug_struct("HtmlRewriter").finish()
104 }
105}
106
107impl Handler for HtmlRewriter {
108 async fn before_send(&self, mut conn: Conn) -> Conn {
109 let html = conn
110 .response_headers()
111 .get_str(ContentType)
112 .and_then(|c| Mime::from_str(c).ok())
113 .map(|m| m.subtype() == "html")
114 .unwrap_or_default();
115
116 if html && let Some(body) = conn.take_response_body() {
117 let settings = self.settings.call(&conn).await;
118 let reader = rewrite(body, settings);
119 conn.response_headers_mut().remove(ContentLength); // we no longer know the content length, if we ever did
120 conn.with_body(Body::new_streaming(reader, None))
121 } else {
122 conn
123 }
124 }
125}
126
127impl HtmlRewriter {
128 /// Construct a new html rewriter that applies the same rewrite to every response.
129 ///
130 /// A function — rather than a `Settings` value — is required because `lol-html`'s content
131 /// handlers are single-use; it is invoked once per rewritten response to produce a fresh set
132 /// of handlers. Build the settings with [`Settings::new_send()`] as the base (its handlers are
133 /// `Send`, as required here) and populate `element_content_handlers` /
134 /// `document_content_handlers`. See [`lol_async::html::Settings`] and the
135 /// [`lol-html`](https://docs.rs/lol-html) docs for the full rewriting API.
136 ///
137 /// ```
138 /// # use trillium_html_rewriter::{HtmlRewriter, Settings, html::{element, html_content::ContentType}};
139 /// HtmlRewriter::new(|| {
140 /// Settings::new_send().append_element_content_handler(element!("body", |el| {
141 /// el.prepend(r#"<script src="/analytics.js"></script>"#, ContentType::Html);
142 /// Ok(())
143 /// }))
144 /// });
145 /// ```
146 ///
147 /// To vary the rewrite based on the request or response, see
148 /// [`new_with_conn`](Self::new_with_conn); to await async work while building the settings,
149 /// see [`new_async`](Self::new_async).
150 pub fn new(f: impl Fn() -> Settings<'static, 'static> + Send + Sync + 'static) -> Self {
151 Self {
152 settings: Arc::new(move |_: &Conn| ready(f())),
153 }
154 }
155
156 /// Construct a new html rewriter from a function that builds [`Settings`] for a given
157 /// [`Conn`].
158 ///
159 /// The function receives the conn whose response is about to be rewritten, so the rewrite can
160 /// depend on the request path, headers, or [state](Conn::state). Like all three constructors,
161 /// the function is invoked once per rewritten response — see [`new`](Self::new) for why a
162 /// function is required and how to build the settings — and it only runs for responses that
163 /// are actually rewritten, so no work is done for non-html responses.
164 ///
165 /// Data read from the conn must be *moved* into the content handlers, which outlive the conn
166 /// borrow:
167 ///
168 /// ```
169 /// # use trillium_html_rewriter::{HtmlRewriter, Settings, html::{element, html_content::ContentType}};
170 /// HtmlRewriter::new_with_conn(|conn| {
171 /// let path = conn.path().to_string();
172 /// Settings::new_send().append_element_content_handler(element!("head", move |el| {
173 /// el.prepend(&format!(r#"<link rel="canonical" href="{path}">"#), ContentType::Html);
174 /// Ok(())
175 /// }))
176 /// });
177 /// ```
178 ///
179 /// To await async work while building the settings, see [`new_async`](Self::new_async).
180 pub fn new_with_conn(
181 f: impl Fn(&Conn) -> Settings<'static, 'static> + Send + Sync + 'static,
182 ) -> Self {
183 Self {
184 settings: Arc::new(move |conn: &Conn| ready(f(conn))),
185 }
186 }
187
188 /// Construct a new html rewriter from an async function that builds [`Settings`] for a given
189 /// [`Conn`].
190 ///
191 /// Like [`new_with_conn`](Self::new_with_conn), the function receives the conn whose response
192 /// is about to be rewritten; because it is async, it can also await while borrowing the conn,
193 /// so the settings can incorporate the result of async work such as a database query or an
194 /// http request. Like all three constructors, the function is invoked once per rewritten
195 /// response — see [`new`](Self::new) for why a function is required and how to build the
196 /// settings.
197 ///
198 /// # Supported forms
199 ///
200 /// The parameter type annotation `: &Conn` is required on closures — inference cannot supply
201 /// it through the higher-ranked [`SettingsFn`] bound.
202 ///
203 /// An async closure (or equivalently a named `async fn(&Conn) -> Settings<'static, 'static>`)
204 /// may borrow the conn across `.await`, but must not capture its environment:
205 ///
206 /// ```
207 /// # use trillium_html_rewriter::{HtmlRewriter, Settings, html::{element, html_content::ContentType}};
208 /// # use trillium::Conn;
209 /// # async fn canonical_url(path: &str) -> String { format!("https://example.com{path}") }
210 /// HtmlRewriter::new_async(async |conn: &Conn| {
211 /// let url = canonical_url(conn.path()).await;
212 /// Settings::new_send().append_element_content_handler(element!("head", move |el| {
213 /// el.prepend(&format!(r#"<link rel="canonical" href="{url}">"#), ContentType::Html);
214 /// Ok(())
215 /// }))
216 /// });
217 /// ```
218 ///
219 /// To use captured state (a client handle, configuration, …), write a plain closure that
220 /// clones what it needs — from its environment and from the conn — into an async block it
221 /// returns. In this form the future cannot borrow the conn:
222 ///
223 /// ```
224 /// # use trillium_html_rewriter::{HtmlRewriter, Settings, html::{element, html_content::ContentType}};
225 /// # use trillium::Conn;
226 /// # #[derive(Clone)] struct Client;
227 /// # impl Client { async fn fetch_banner(&self, path: String) -> String { path } }
228 /// # let client = Client;
229 /// HtmlRewriter::new_async(move |conn: &Conn| {
230 /// let client = client.clone();
231 /// let path = conn.path().to_string();
232 /// async move {
233 /// let banner = client.fetch_banner(path).await;
234 /// Settings::new_send().append_element_content_handler(element!("body", move |el| {
235 /// el.prepend(&banner, ContentType::Html);
236 /// Ok(())
237 /// }))
238 /// }
239 /// });
240 /// ```
241 ///
242 /// Note that in either form, data destined for the content handlers must be *moved* into
243 /// them, as they outlive the settings-building future.
244 pub fn new_async<F>(f: F) -> Self
245 where
246 F: for<'a> SettingsFn<'a>,
247 {
248 Self {
249 settings: Arc::new(f),
250 }
251 }
252}