#![forbid(unsafe_code)]
#![deny(
clippy::dbg_macro,
missing_copy_implementations,
rustdoc::missing_crate_level_docs,
missing_debug_implementations,
missing_docs,
nonstandard_style,
unused_qualifications
)]
#![doc = include_str!("../README.md")]
use lol_async::rewrite;
pub use lol_async::{Settings, html};
use mime::Mime;
use std::{
fmt::{self, Debug, Formatter},
future::{Future, ready},
pin::Pin,
str::FromStr,
sync::Arc,
};
use trillium::{
Body, Conn, Handler,
KnownHeaderName::{ContentLength, ContentType},
};
pub struct HtmlRewriter {
settings: Arc<dyn ErasedSettingsFn>,
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not an async settings builder",
label = "expected an async function from `&Conn` to `Settings<'static, 'static>`",
note = "if the settings don't require awaiting anything, use `HtmlRewriter::new` or \
`HtmlRewriter::new_with_conn` instead of `HtmlRewriter::new_async`",
note = "write an async closure with an annotated parameter: `async |conn: &Conn| {{ .. }}`, \
not `|conn| async move {{ .. }}` — and the `&Conn` annotation is required for \
inference",
note = "async closures that capture state don't implement `Fn`; to use captured state, write \
a plain closure returning an async block that owns its data: `move |conn: &Conn| {{ \
let data = data.clone(); async move {{ .. }} }}`",
note = "the returned future must be `Send`"
)]
pub trait SettingsFn<'a>: Send + Sync + 'static {
type Fut: Future<Output = Settings<'static, 'static>> + Send + 'a;
fn call(&self, conn: &'a Conn) -> Self::Fut;
}
impl<'a, F, Fut> SettingsFn<'a> for F
where
F: Fn(&'a Conn) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Settings<'static, 'static>> + Send + 'a,
{
type Fut = Fut;
fn call(&self, conn: &'a Conn) -> Fut {
self(conn)
}
}
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
trait ErasedSettingsFn: Send + Sync {
fn call<'a>(&'a self, conn: &'a Conn) -> BoxFuture<'a, Settings<'static, 'static>>;
}
impl<F> ErasedSettingsFn for F
where
F: for<'a> SettingsFn<'a>,
{
fn call<'a>(&'a self, conn: &'a Conn) -> BoxFuture<'a, Settings<'static, 'static>> {
Box::pin(SettingsFn::call(self, conn))
}
}
impl Debug for HtmlRewriter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("HtmlRewriter").finish()
}
}
impl Handler for HtmlRewriter {
async fn before_send(&self, mut conn: Conn) -> Conn {
let html = conn
.response_headers()
.get_str(ContentType)
.and_then(|c| Mime::from_str(c).ok())
.map(|m| m.subtype() == "html")
.unwrap_or_default();
if html && let Some(body) = conn.take_response_body() {
let settings = self.settings.call(&conn).await;
let reader = rewrite(body, settings);
conn.response_headers_mut().remove(ContentLength); conn.with_body(Body::new_streaming(reader, None))
} else {
conn
}
}
}
impl HtmlRewriter {
pub fn new(f: impl Fn() -> Settings<'static, 'static> + Send + Sync + 'static) -> Self {
Self {
settings: Arc::new(move |_: &Conn| ready(f())),
}
}
pub fn new_with_conn(
f: impl Fn(&Conn) -> Settings<'static, 'static> + Send + Sync + 'static,
) -> Self {
Self {
settings: Arc::new(move |conn: &Conn| ready(f(conn))),
}
}
pub fn new_async<F>(f: F) -> Self
where
F: for<'a> SettingsFn<'a>,
{
Self {
settings: Arc::new(f),
}
}
}