maud/
lib.rs

1#![no_std]
2
3//! A macro for writing HTML templates.
4//!
5//! This documentation only describes the runtime API. For a general
6//! guide, check out the [book] instead.
7//!
8//! [book]: https://maud.lambda.xyz/
9
10#![doc(html_root_url = "https://docs.rs/maud/0.26.0")]
11
12extern crate alloc;
13
14pub use wini_maud_macros::html;
15use {
16    alloc::{borrow::Cow, boxed::Box, string::String, sync::Arc},
17    core::{
18        fmt::{self, Arguments, Display, Write},
19        ops::Deref,
20    },
21    hashbrown::HashSet,
22};
23
24mod escape;
25
26/// An adapter that escapes HTML special characters.
27///
28/// The following characters are escaped:
29///
30/// * `&` is escaped as `&`
31/// * `<` is escaped as `&lt;`
32/// * `>` is escaped as `&gt;`
33/// * `"` is escaped as `&quot;`
34///
35/// All other characters are passed through unchanged.
36///
37/// **Note:** In versions prior to 0.13, the single quote (`'`) was
38/// escaped as well.
39///
40/// # Example
41///
42/// ```rust
43/// use maud::Escaper;
44/// use std::fmt::Write;
45/// let mut s = String::new();
46/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap();
47/// assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
48/// ```
49pub struct Escaper<'a>(&'a mut String);
50
51impl<'a> Escaper<'a> {
52    /// Creates an `Escaper` from a `String`.
53    pub fn new(buffer: &'a mut String) -> Escaper<'a> {
54        Escaper(buffer)
55    }
56}
57
58impl fmt::Write for Escaper<'_> {
59    fn write_str(&mut self, s: &str) -> fmt::Result {
60        escape::escape_to_string(s, self.0);
61        Ok(())
62    }
63}
64
65/// Represents a type that can be rendered as HTML.
66///
67/// To implement this for your own type, override either the `.render()`
68/// or `.render_to()` methods; since each is defined in terms of the
69/// other, you only need to implement one of them. See the example below.
70///
71/// # Minimal implementation
72///
73/// An implementation of this trait must override at least one of
74/// `.render()` or `.render_to()`. Since the default definitions of
75/// these methods call each other, not doing this will result in
76/// infinite recursion.
77///
78/// # Example
79///
80/// ```rust
81/// use maud::{html, Markup, Render};
82///
83/// /// Provides a shorthand for linking to a CSS stylesheet.
84/// pub struct Stylesheet(&'static str);
85///
86/// impl Render for Stylesheet {
87///     fn render(&self) -> Markup {
88///         html! {
89///             link rel="stylesheet" type="text/css" href=(self.0);
90///         }
91///     }
92/// }
93/// ```
94pub trait Render {
95    /// Renders `self` as a block of `Markup`.
96    fn render(&self) -> Markup {
97        let mut buffer = String::new();
98        self.render_to(&mut buffer);
99        Markup {
100            content: PreEscaped(buffer),
101            linked_files: HashSet::new(),
102        }
103    }
104
105    /// Appends a representation of `self` to the given buffer.
106    ///
107    /// Its default implementation just calls `.render()`, but you may
108    /// override it with something more efficient.
109    ///
110    /// Note that no further escaping is performed on data written to
111    /// the buffer. If you override this method, you must make sure that
112    /// any data written is properly escaped, whether by hand or using
113    /// the [`Escaper`](struct.Escaper.html) wrapper struct.
114    fn render_to(&self, buffer: &mut String) {
115        buffer.push_str(&self.render().content.into_string());
116    }
117}
118
119impl Render for str {
120    fn render_to(&self, w: &mut String) {
121        escape::escape_to_string(self, w);
122    }
123}
124
125impl Render for String {
126    fn render_to(&self, w: &mut String) {
127        str::render_to(self, w);
128    }
129}
130
131impl Render for Cow<'_, str> {
132    fn render_to(&self, w: &mut String) {
133        str::render_to(self, w);
134    }
135}
136
137impl Render for Arguments<'_> {
138    fn render_to(&self, w: &mut String) {
139        let _ = Escaper::new(w).write_fmt(*self);
140    }
141}
142
143impl<T: Render + ?Sized> Render for &T {
144    fn render_to(&self, w: &mut String) {
145        T::render_to(self, w);
146    }
147}
148
149impl<T: Render + ?Sized> Render for &mut T {
150    fn render_to(&self, w: &mut String) {
151        T::render_to(self, w);
152    }
153}
154
155impl<T: Render + ?Sized> Render for Box<T> {
156    fn render_to(&self, w: &mut String) {
157        T::render_to(self, w);
158    }
159}
160
161impl<T: Render + ?Sized> Render for Arc<T> {
162    fn render_to(&self, w: &mut String) {
163        T::render_to(self, w);
164    }
165}
166
167macro_rules! impl_render_with_display {
168    ($($ty:ty)*) => {
169        $(
170            impl Render for $ty {
171                fn render_to(&self, w: &mut String) {
172                    // TODO: remove the explicit arg when Rust 1.58 is released
173                    format_args!("{self}", self = self).render_to(w);
174                }
175            }
176        )*
177    };
178}
179
180impl_render_with_display! {
181    char f32 f64
182}
183
184macro_rules! impl_render_with_itoa {
185    ($($ty:ty)*) => {
186        $(
187            impl Render for $ty {
188                fn render_to(&self, w: &mut String) {
189                    w.push_str(itoa::Buffer::new().format(*self));
190                }
191            }
192        )*
193    };
194}
195
196impl_render_with_itoa! {
197    i8 i16 i32 i64 i128 isize
198    u8 u16 u32 u64 u128 usize
199}
200
201/// Renders a value using its [`Display`] impl.
202///
203/// # Example
204///
205/// ```rust
206/// use maud::html;
207/// use std::net::Ipv4Addr;
208///
209/// let ip_address = Ipv4Addr::new(127, 0, 0, 1);
210///
211/// let markup = html! {
212///     "My IP address is: "
213///     (maud::display(ip_address))
214/// };
215///
216/// assert_eq!(markup.into_string(), "My IP address is: 127.0.0.1");
217/// ```
218pub fn display(value: impl Display) -> impl Render {
219    struct DisplayWrapper<T>(T);
220
221    impl<T: Display> Render for DisplayWrapper<T> {
222        fn render_to(&self, w: &mut String) {
223            format_args!("{0}", self.0).render_to(w);
224        }
225    }
226
227    DisplayWrapper(value)
228}
229
230/// A wrapper that renders the inner value without escaping.
231#[derive(Debug, Clone, Copy)]
232pub struct PreEscaped<T>(pub T);
233
234impl<T: AsRef<str>> Render for PreEscaped<T> {
235    fn render_to(&self, w: &mut String) {
236        w.push_str(self.0.as_ref());
237    }
238}
239
240/// A block of markup is a string that does not need to be escaped.
241///
242/// The `html!` macro expands to an expression of this type.
243#[derive(Debug, Default, Clone)]
244pub struct Markup {
245    pub content: PreEscaped<String>,
246    pub linked_files: HashSet<String>,
247}
248impl Markup {
249    /// Converts the inner value to a string.
250    pub fn into_string(self) -> String {
251        self.content.into()
252    }
253}
254
255impl Render for Markup {
256    fn render_to(&self, w: &mut String) {
257        w.push_str(&self.content.0)
258    }
259}
260
261impl From<Markup> for PreEscaped<String> {
262    fn from(val: Markup) -> Self {
263        val.content
264    }
265}
266
267
268impl Deref for Markup {
269    type Target = PreEscaped<String>;
270
271    fn deref(&self) -> &Self::Target {
272        &self.content
273    }
274}
275
276impl<T: Into<String>> PreEscaped<T> {
277    /// Converts the inner value to a string.
278    pub fn into_string(self) -> String {
279        self.0.into()
280    }
281}
282
283impl<T: Into<String>> From<PreEscaped<T>> for String {
284    fn from(value: PreEscaped<T>) -> String {
285        value.into_string()
286    }
287}
288
289impl<T: Default> Default for PreEscaped<T> {
290    fn default() -> Self {
291        Self(Default::default())
292    }
293}
294
295/// The literal string `<!DOCTYPE html>`.
296///
297/// # Example
298///
299/// A minimal web page:
300///
301/// ```rust
302/// use maud::{DOCTYPE, html};
303///
304/// let markup = html! {
305///     (DOCTYPE)
306///     html {
307///         head {
308///             meta charset="utf-8";
309///             title { "Test page" }
310///         }
311///         body {
312///             p { "Hello, world!" }
313///         }
314///     }
315/// };
316/// ```
317pub const DOCTYPE: PreEscaped<&'static str> = PreEscaped("<!DOCTYPE html>");
318
319#[cfg(feature = "rocket")]
320mod rocket_support {
321    extern crate std;
322
323    use {
324        crate::PreEscaped,
325        alloc::string::String,
326        rocket::{
327            http::ContentType,
328            request::Request,
329            response::{Responder, Response},
330        },
331        std::io::Cursor,
332    };
333
334    impl Responder<'_, 'static> for PreEscaped<String> {
335        fn respond_to(self, _: &Request) -> rocket::response::Result<'static> {
336            Response::build()
337                .header(ContentType::HTML)
338                .sized_body(self.0.len(), Cursor::new(self.0))
339                .ok()
340        }
341    }
342}
343
344#[cfg(feature = "actix-web")]
345mod actix_support {
346    use {
347        crate::PreEscaped,
348        actix_web_dep::{
349            HttpRequest,
350            HttpResponse,
351            Responder,
352            body::{BodySize, MessageBody},
353            http::header,
354            web::Bytes,
355        },
356        alloc::string::String,
357        core::{
358            pin::Pin,
359            task::{Context, Poll},
360        },
361    };
362
363    impl MessageBody for PreEscaped<String> {
364        type Error = <String as MessageBody>::Error;
365
366        fn size(&self) -> BodySize {
367            self.0.size()
368        }
369
370        fn poll_next(
371            mut self: Pin<&mut Self>,
372            cx: &mut Context<'_>,
373        ) -> Poll<Option<Result<Bytes, Self::Error>>> {
374            Pin::new(&mut self.0).poll_next(cx)
375        }
376    }
377
378    impl Responder for PreEscaped<String> {
379        type Body = String;
380
381        fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
382            HttpResponse::Ok()
383                .content_type(header::ContentType::html())
384                .message_body(self.0)
385                .unwrap()
386        }
387    }
388}
389
390#[cfg(feature = "tide")]
391mod tide_support {
392    use {
393        crate::PreEscaped,
394        alloc::string::String,
395        tide::{Response, StatusCode, http::mime},
396    };
397
398    impl From<PreEscaped<String>> for Response {
399        fn from(markup: PreEscaped<String>) -> Response {
400            Response::builder(StatusCode::Ok)
401                .body(markup.into_string())
402                .content_type(mime::HTML)
403                .build()
404        }
405    }
406}
407
408#[cfg(feature = "axum")]
409mod axum_support {
410    use {
411        crate::Markup,
412        axum_core::response::{IntoResponse, Response},
413        http::{HeaderMap, HeaderValue, header},
414    };
415
416    impl IntoResponse for Markup {
417        fn into_response(self) -> Response {
418            let mut headers = HeaderMap::new();
419            headers.insert(
420                header::CONTENT_TYPE,
421                HeaderValue::from_static("text/html; charset=utf-8"),
422            );
423            (headers, self.content.0).into_response()
424        }
425    }
426}
427
428#[cfg(feature = "warp")]
429mod warp_support {
430    use {
431        crate::PreEscaped,
432        alloc::string::String,
433        warp::reply::{self, Reply, Response},
434    };
435
436    impl Reply for PreEscaped<String> {
437        fn into_response(self) -> Response {
438            reply::html(self.into_string()).into_response()
439        }
440    }
441}
442
443#[cfg(feature = "submillisecond")]
444mod submillisecond_support {
445    use {
446        crate::PreEscaped,
447        alloc::string::String,
448        submillisecond::{
449            http::{HeaderMap, HeaderValue, header},
450            response::{IntoResponse, Response},
451        },
452    };
453
454    impl IntoResponse for PreEscaped<String> {
455        fn into_response(self) -> Response {
456            let mut headers = HeaderMap::new();
457            headers.insert(
458                header::CONTENT_TYPE,
459                HeaderValue::from_static("text/html; charset=utf-8"),
460            );
461            (headers, self.0).into_response()
462        }
463    }
464}
465
466
467
468#[doc(hidden)]
469pub mod macro_private {
470    pub use hashbrown::HashSet;
471    use {
472        crate::{Render, display},
473        alloc::string::String,
474        core::fmt::Display,
475    };
476
477    #[doc(hidden)]
478    #[macro_export]
479    macro_rules! render_to {
480        ($x:expr, $buffer:expr) => {{
481            use $crate::macro_private::*;
482            match ChooseRenderOrDisplay($x) {
483                x => (&&x).implements_render_or_display().render_to(x.0, $buffer),
484            }
485        }};
486    }
487
488    pub use render_to;
489
490    pub struct ChooseRenderOrDisplay<T>(pub T);
491
492    pub struct ViaRenderTag;
493    pub struct ViaDisplayTag;
494
495    pub trait ViaRender {
496        fn implements_render_or_display(&self) -> ViaRenderTag {
497            ViaRenderTag
498        }
499    }
500    pub trait ViaDisplay {
501        fn implements_render_or_display(&self) -> ViaDisplayTag {
502            ViaDisplayTag
503        }
504    }
505
506    impl<T: Render> ViaRender for &ChooseRenderOrDisplay<T> {}
507    impl<T: Display> ViaDisplay for ChooseRenderOrDisplay<T> {}
508
509    impl ViaRenderTag {
510        pub fn render_to<T: Render + ?Sized>(self, value: &T, buffer: &mut String) {
511            value.render_to(buffer);
512        }
513    }
514
515    impl ViaDisplayTag {
516        pub fn render_to<T: Display + ?Sized>(self, value: &T, buffer: &mut String) {
517            display(value).render_to(buffer);
518        }
519    }
520}