Skip to main content

lol_async/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    missing_copy_implementations,
4    rustdoc::missing_crate_level_docs,
5    missing_debug_implementations,
6    missing_docs,
7    nonstandard_style,
8    unused_qualifications
9)]
10
11//! Async wrapper for
12//! [`cloudflare/lol-html`](https://github.com/cloudflare/lol-html).
13//!
14//! ```
15//! # use async_global_executor as your_async_executor;
16//! # use futures_lite::{io::Cursor, AsyncReadExt};
17//! use lol_async::html::{element, html_content::ContentType, send::Settings};
18//!
19//! # your_async_executor::block_on(async {
20//! let input = Cursor::new(
21//!     "<html><head><title>hello lol</title></head><body><h1>hey there</h1></body></html>",
22//! );
23//!
24//! let mut reader = lol_async::rewrite(
25//!     input,
26//!     Settings::new_send().append_element_content_handler(element!("h1", |el| {
27//!         el.append("<span>this was inserted</span>", ContentType::Html);
28//!         Ok(())
29//!     })),
30//! );
31//!
32//! let mut buf = String::new();
33//! reader.read_to_string(&mut buf).await?;
34//!
35//! assert_eq!(
36//!     buf,
37//!     "<html><head><title>hello lol</title></head><body><h1>hey there<span>this was \
38//!      inserted</span></h1></body></html>"
39//! );
40//! #     Result::<_, Box<dyn std::error::Error>>::Ok(()) }).unwrap();
41//! ```
42
43use futures_lite::AsyncRead;
44pub use lol_html as html;
45pub use lol_html::send::Settings;
46use lol_html::{OutputSink, send::HtmlRewriter};
47use pin_project_lite::pin_project;
48use std::{
49    collections::VecDeque,
50    fmt::{self, Debug},
51    io::{self, Read},
52    pin::Pin,
53    sync::{Arc, Mutex},
54    task::{Context, Poll, ready},
55};
56
57#[derive(Clone)]
58struct OutputBuffer(Arc<Mutex<VecDeque<u8>>>);
59
60impl OutputSink for OutputBuffer {
61    fn handle_chunk(&mut self, chunk: &[u8]) {
62        self.0.lock().unwrap().extend(chunk);
63    }
64}
65
66pin_project! {
67    /// An [`AsyncRead`] adapter that streams rewritten HTML.
68    ///
69    /// Created by [`rewrite`]. Reads from the inner source, feeds data
70    /// through the [`lol_html::HtmlRewriter`], and yields rewritten output.
71    pub struct Rewriter<'h, R> {
72        #[pin] source: R,
73        rewriter: Option<HtmlRewriter<'h, OutputBuffer>>,
74        output: OutputBuffer,
75        read_buf: Vec<u8>,
76    }
77}
78
79impl<R> Debug for Rewriter<'_, R> {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.debug_struct("Rewriter")
82            .field("output_len", &self.output.0.lock().unwrap().len())
83            .field("done", &self.rewriter.is_none())
84            .finish()
85    }
86}
87
88/// Creates an [`AsyncRead`] that streams HTML rewritten according to the
89/// provided [`Settings`](lol_html::send::Settings).
90///
91/// See the [crate-level documentation](crate) for an example.
92pub fn rewrite<'h, R: AsyncRead>(source: R, settings: Settings<'h, '_>) -> Rewriter<'h, R> {
93    let output = OutputBuffer(Arc::new(Mutex::new(VecDeque::new())));
94    let rewriter = HtmlRewriter::new(settings, output.clone());
95    Rewriter {
96        source,
97        rewriter: Some(rewriter),
98        output,
99        read_buf: vec![0; 1024],
100    }
101}
102
103impl<R: AsyncRead> AsyncRead for Rewriter<'_, R> {
104    fn poll_read(
105        self: Pin<&mut Self>,
106        cx: &mut Context<'_>,
107        buf: &mut [u8],
108    ) -> Poll<io::Result<usize>> {
109        let mut this = self.project();
110
111        loop {
112            // Drain any buffered output first
113            {
114                let mut output = this.output.0.lock().unwrap();
115                if !output.is_empty() {
116                    let n = Read::read(&mut *output, buf)?;
117                    return Poll::Ready(Ok(n));
118                }
119            }
120
121            // If the rewriter is done and no output remains, signal EOF
122            let rewriter = match this.rewriter.as_mut() {
123                Some(r) => r,
124                None => return Poll::Ready(Ok(0)),
125            };
126
127            // Read from the source and feed through the rewriter
128            match ready!(this.source.as_mut().poll_read(cx, this.read_buf)) {
129                Ok(0) => {
130                    this.rewriter
131                        .take()
132                        .unwrap()
133                        .end()
134                        .map_err(io::Error::other)?;
135                    // Loop back to drain any final output
136                }
137
138                Ok(n) => {
139                    rewriter
140                        .write(&this.read_buf[..n])
141                        .map_err(io::Error::other)?;
142                    // Loop back to drain output
143                }
144
145                Err(e) => return Poll::Ready(Err(e)),
146            }
147        }
148    }
149}