maud/
lib.rs

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