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