russx/
lib.rs

1//! # Russx
2//! Russx implements a template rendering engine based on [rstml](https://github.com/rs-tml/rstml).
3//! It generates Rust code from your templates at compile time using a macro.
4//! This crate is inpired by both [Askama](https://github.com/djc/askama) and [Leptos](https://github.com/leptos-rs/leptos).
5//!
6//! To create templates use the [`templates`](macro.templates.html) macro,
7//! or the [`tmpl`](macro.tmpl.html) for dynamic templates.
8
9use std::{error::Error as StdError, fmt, io, ops};
10
11pub use russx_macros::{templates, tmpl};
12
13pub type Result<T, E = Error> = core::result::Result<T, E>;
14
15/// Russx error type.
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum Error {
19    /// Formatting error
20    #[error("formatting error: {0}")]
21    Fmt(#[from] fmt::Error),
22    /// HTML attribute name error.
23    /// Fired when an attribute name doesn't conform to the [HTML standard](https://html.spec.whatwg.org/#attributes-2).
24    #[error("attribute doesn't conform to html standard: {0:?}")]
25    AttributeError(String),
26    /// Any other error that might be raised inside a template.
27    /// use `result.map_err(Error::custom)?`
28    #[error("custom error: {0}")]
29    Custom(Box<dyn StdError + Send + Sync>),
30}
31
32impl Error {
33    /// A utility function to create `Error::Custom`.
34    pub fn custom(err: impl StdError + Send + Sync + 'static) -> Self {
35        Self::Custom(err.into())
36    }
37}
38
39impl From<io::Error> for Error {
40    fn from(err: io::Error) -> Self {
41        Self::custom(err)
42    }
43}
44
45#[doc(hidden)]
46pub mod __typed_builder {
47    pub use typed_builder::*;
48}
49
50#[doc(hidden)]
51/// Writes html-escaped `value` into `writer`.
52pub fn __write_escaped(
53    writer: &mut (impl fmt::Write + ?Sized),
54    value: &(impl fmt::Display + ?Sized),
55) -> Result<()> {
56    use fmt::Write;
57
58    pub struct EscapeWriter<'a, W: fmt::Write + ?Sized>(&'a mut W);
59
60    impl<W: fmt::Write + ?Sized> fmt::Write for EscapeWriter<'_, W> {
61        #[inline]
62        fn write_str(&mut self, s: &str) -> fmt::Result {
63            use askama_escape::Escaper;
64
65            askama_escape::Html.write_escaped(&mut *self.0, s)
66        }
67    }
68
69    write!(EscapeWriter(writer), "{value}")?;
70
71    Ok(())
72}
73
74/// Write an attribute and check its validity.
75fn write_attribute(
76    writer: &mut (impl fmt::Write + ?Sized),
77    value: &(impl fmt::Display + ?Sized),
78) -> Result<()> {
79    use fmt::Write;
80
81    pub struct AttributeWriter<'a, W: fmt::Write + ?Sized> {
82        has_written: bool,
83        writer: &'a mut W,
84    }
85
86    impl<W: fmt::Write + ?Sized> fmt::Write for AttributeWriter<'_, W> {
87        fn write_str(&mut self, s: &str) -> fmt::Result {
88            #[rustfmt::skip]
89            fn is_invalid_attribute_char(ch: char) -> bool {
90                matches!(
91                    ch,
92                    '\0'..='\x1F' | '\x7F'..='\u{9F}'
93                    | ' ' | '"' | '\'' | '>' | '/' | '='
94                    | '\u{FDD0}'..='\u{FDEF}'
95                    | '\u{0FFFE}' | '\u{0FFFF}' | '\u{01FFFE}' | '\u{01FFFF}' | '\u{2FFFE}'
96                    | '\u{2FFFF}' | '\u{3FFFE}' | '\u{03FFFF}' | '\u{04FFFE}' | '\u{4FFFF}'
97                    | '\u{5FFFE}' | '\u{5FFFF}' | '\u{06FFFE}' | '\u{06FFFF}' | '\u{7FFFE}'
98                    | '\u{7FFFF}' | '\u{8FFFE}' | '\u{08FFFF}' | '\u{09FFFE}' | '\u{9FFFF}'
99                    | '\u{AFFFE}' | '\u{AFFFF}' | '\u{0BFFFE}' | '\u{0BFFFF}' | '\u{CFFFE}'
100                    | '\u{CFFFF}' | '\u{DFFFE}' | '\u{0DFFFF}' | '\u{0EFFFE}' | '\u{EFFFF}'
101                    | '\u{FFFFE}' | '\u{FFFFF}' | '\u{10FFFE}' | '\u{10FFFF}'
102                )
103            }
104
105            self.has_written |= !s.is_empty();
106            if s.contains(is_invalid_attribute_char) {
107                return Err(fmt::Error);
108            }
109            self.writer.write_str(s)
110        }
111    }
112
113    let mut attr_writer = AttributeWriter {
114        has_written: false,
115        writer,
116    };
117
118    write!(attr_writer, "{value}")?;
119
120    if !attr_writer.has_written {
121        return Err(Error::AttributeError(value.to_string()));
122    }
123
124    Ok(())
125}
126
127mod sealed {
128    pub trait SealedAttribute {}
129    pub trait SealedAttributes {}
130}
131
132/// The attribute trait, this will write a single attribute.
133pub trait Attribute: sealed::SealedAttribute {
134    /// Renders the attribute to the given fmt writer, the attribute will have a space prefixed.
135    fn render_into(&self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()>;
136}
137
138impl<T: Attribute + ?Sized> sealed::SealedAttribute for &'_ T {}
139impl<T: Attribute + ?Sized> Attribute for &'_ T {
140    #[inline]
141    fn render_into(&self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
142        T::render_into(self, writer)
143    }
144}
145
146impl<T: Attribute + ?Sized> sealed::SealedAttribute for &'_ mut T {}
147impl<T: Attribute + ?Sized> Attribute for &'_ mut T {
148    #[inline]
149    fn render_into(&self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
150        T::render_into(self, writer)
151    }
152}
153
154impl sealed::SealedAttribute for String {}
155impl Attribute for String {
156    /// Writes a valueless attribute
157    fn render_into(&self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
158        writer.write_char(' ')?;
159        write_attribute(writer, self)?;
160
161        Ok(())
162    }
163}
164
165impl sealed::SealedAttribute for str {}
166impl Attribute for str {
167    /// Writes a valueless attribute
168    fn render_into(&self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
169        writer.write_char(' ')?;
170        write_attribute(writer, self)?;
171
172        Ok(())
173    }
174}
175
176impl<N: fmt::Display, T: fmt::Display> sealed::SealedAttribute for (N, T) {}
177impl<N: fmt::Display, T: fmt::Display> Attribute for (N, T) {
178    fn render_into(&self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
179        writer.write_char(' ')?;
180        write_attribute(writer, &self.0)?;
181        writer.write_str("=\"")?;
182        __write_escaped(writer, &self.1)?;
183        writer.write_char('"')?;
184
185        Ok(())
186    }
187}
188
189/// The attributes trait, this can write a variable amount of attributes.
190/// You can use this within a template using a braced attribute, `{...}`.
191///
192/// ```rust
193/// let style_attr = ("style", "border: 1px solid black");
194/// let html = russx::tmpl! {
195///     <div {style_attr}>
196///         "hello world"
197///     </div>
198/// }.render()?;
199/// ```
200pub trait Attributes: sealed::SealedAttributes {
201    /// Renders the attributes to the given fmt writer, the attributes will be separated by spaces
202    /// and be prefixed with a space.
203    fn render_into(self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()>;
204}
205
206impl<T: Attribute> sealed::SealedAttributes for T {}
207impl<T: Attribute> Attributes for T {
208    fn render_into(self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
209        Attribute::render_into(&self, writer)
210    }
211}
212
213impl sealed::SealedAttributes for () {}
214impl Attributes for () {
215    /// Does nothing
216    #[inline]
217    fn render_into(self, _writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
218        Ok(())
219    }
220}
221
222impl<I: Attribute, T: IntoIterator<Item = I>> sealed::SealedAttributes for ops::RangeTo<T> {}
223impl<I: Attribute, T: IntoIterator<Item = I>> Attributes for ops::RangeTo<T> {
224    fn render_into(self, writer: &mut (impl fmt::Write + ?Sized)) -> Result<()> {
225        for attr in self.end {
226            attr.render_into(writer)?;
227        }
228
229        Ok(())
230    }
231}
232
233/// Main template trait.
234/// Implementations can be generated using both the `tmpl` and `templates` macros.
235pub trait Template: Sized {
236    /// Provides a rough estimate of the expanded length of the rendered template.
237    /// Larger values result in higher memory usage but fewer reallocations.
238    /// Smaller values result in the opposite. This value only affects render.
239    /// It does not take effect when calling `render_into`, `write_into`, the `fmt::Display`
240    // implementation, or the blanket `ToString::to_string` implementation.
241    const SIZE_HINT: usize;
242
243    /// Provides a conservative estimate of the expanded length of the rendered template.
244    /// See `Template::SIZE_HINT` for more information.
245    fn size_hint(&self) -> usize {
246        Self::SIZE_HINT
247    }
248
249    /// Renders the template to the given fmt writer.
250    fn render_into(self, writer: &mut dyn fmt::Write) -> Result<()>;
251
252    /// Helper method which allocates a new String and renders into it.
253    fn render(self) -> Result<String> {
254        let mut buf = String::new();
255        let _ = buf.try_reserve(self.size_hint());
256        self.render_into(&mut buf)?;
257        Ok(buf)
258    }
259
260    /// Renders the template to the given IO writer.
261    #[inline]
262    fn write_into(self, writer: &mut (impl io::Write + ?Sized)) -> io::Result<()> {
263        // Create a shim which translates a Write to a fmt::Write and saves
264        // off I/O errors. instead of discarding them
265        struct Adapter<'a, T: ?Sized + 'a> {
266            inner: &'a mut T,
267            error: io::Result<()>,
268        }
269
270        impl<T: io::Write + ?Sized> fmt::Write for Adapter<'_, T> {
271            fn write_str(&mut self, s: &str) -> fmt::Result {
272                match self.inner.write_all(s.as_bytes()) {
273                    Ok(()) => Ok(()),
274                    Err(e) => {
275                        self.error = Err(e);
276                        Err(fmt::Error)
277                    }
278                }
279            }
280        }
281
282        struct DisplayError;
283        impl fmt::Display for DisplayError {
284            fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
285                Err(fmt::Error)
286            }
287        }
288
289        let mut output = Adapter {
290            inner: writer,
291            error: Ok(()),
292        };
293        match self.render_into(&mut output) {
294            Ok(()) => Ok(()),
295            Err(err) => {
296                // check if the error came from the underlying `Write` or not
297                if output.error.is_err() {
298                    output.error
299                } else {
300                    match err {
301                        Error::Fmt(fmt::Error) => Err(writer
302                            .write_fmt(format_args!("{DisplayError}"))
303                            .unwrap_err()),
304                        Error::AttributeError(_) => {
305                            Err(io::Error::new(io::ErrorKind::InvalidData, err))
306                        }
307                        err => Err(io::Error::new(io::ErrorKind::Other, err)),
308                    }
309                }
310            }
311        }
312    }
313}
314
315type DynRenderInto<'a> = dyn FnOnce(&mut dyn fmt::Write) -> Result<()> + Send + 'a;
316
317/// A dynamic template generated from a function.
318/// This is the type of `<prop _ />` and `children` when instantiating a static template.
319pub struct TemplateFn<'a> {
320    size_hint: usize,
321    render_into: Box<DynRenderInto<'a>>,
322}
323
324impl<'a> Default for TemplateFn<'a> {
325    /// Creates a new `TemplateFn` which does nothing
326    #[inline]
327    fn default() -> Self {
328        Self::new(0, |_| Ok(()))
329    }
330}
331
332impl<'a> TemplateFn<'a> {
333    /// Creates a new `TemplateFn` from a size hint (see `Template::SIZE_HINT`) and a render_into
334    /// function (see `Template::render_into`).
335    pub fn new(
336        size_hint: usize,
337        render_into: impl FnOnce(&mut dyn fmt::Write) -> Result<()> + Send + 'a,
338    ) -> Self {
339        Self {
340            size_hint,
341            render_into: Box::new(render_into),
342        }
343    }
344
345    /// Converts a template into `TemplateFn`, this cannot be done through the `Into` trait since
346    /// `TemplateFn` also implements `Template`.
347    pub fn from_template<T: Template + Send + 'a>(template: T) -> Self {
348        Self::new(template.size_hint(), |writer| template.render_into(writer))
349    }
350}
351
352impl Template for TemplateFn<'_> {
353    const SIZE_HINT: usize = 20;
354
355    fn size_hint(&self) -> usize {
356        self.size_hint
357    }
358
359    fn render_into(self, writer: &mut dyn fmt::Write) -> Result<()> {
360        (self.render_into)(writer)
361    }
362}
363
364impl fmt::Debug for TemplateFn<'_> {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        #[derive(Debug)]
367        struct RenderInto;
368
369        f.debug_struct(std::any::type_name::<Self>())
370            .field("size_hint", &self.size_hint)
371            .field("render_into", &RenderInto)
372            .finish()
373    }
374}
375
376#[allow(dead_code)]
377const HTML_MIME_TYPE: &str = "text/html";
378
379#[doc(hidden)]
380#[cfg(feature = "axum")]
381pub mod __axum {
382    pub use axum_core::response::{IntoResponse, Response};
383    use http::{header, HeaderValue, StatusCode};
384
385    use super::*;
386
387    pub fn into_response<T: Template>(t: T) -> Response {
388        match t.render() {
389            Ok(body) => IntoResponse::into_response((
390                [(
391                    header::CONTENT_TYPE,
392                    HeaderValue::from_static(HTML_MIME_TYPE),
393                )],
394                body,
395            )),
396            Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
397        }
398    }
399
400    impl<'a> IntoResponse for TemplateFn<'a> {
401        fn into_response(self) -> Response {
402            into_response(self)
403        }
404    }
405}
406
407#[doc(hidden)]
408#[cfg(feature = "actix-web")]
409pub mod __actix_web {
410    pub use actix_web::{body::BoxBody, HttpRequest, HttpResponse, Responder};
411    use actix_web::{
412        http::{header::HeaderValue, StatusCode},
413        HttpResponseBuilder, ResponseError,
414    };
415
416    use super::*;
417
418    impl ResponseError for Error {}
419
420    pub fn respond_to<T: Template>(t: T) -> HttpResponse {
421        match t.render() {
422            Ok(body) => HttpResponseBuilder::new(StatusCode::OK)
423                .content_type(HeaderValue::from_static(HTML_MIME_TYPE))
424                .body(body),
425            Err(err) => HttpResponse::from_error(err),
426        }
427    }
428
429    impl<'a> Responder for TemplateFn<'a> {
430        type Body = BoxBody;
431
432        fn respond_to(self, _req: &HttpRequest) -> HttpResponse {
433            respond_to(self)
434        }
435    }
436}
437
438#[doc(hidden)]
439#[cfg(feature = "hyper")]
440pub mod __hyper {
441    use hyper::{
442        header::{self, HeaderValue},
443        StatusCode,
444    };
445
446    use super::*;
447
448    pub type Body = String;
449    pub type Response<B = Body> = hyper::Response<B>;
450
451    fn try_respond<T: Template>(t: T) -> Result<Response> {
452        Ok(Response::builder()
453            .status(StatusCode::OK)
454            .header(
455                header::CONTENT_TYPE,
456                HeaderValue::from_static(HTML_MIME_TYPE),
457            )
458            .body(t.render()?.into())
459            .unwrap())
460    }
461
462    pub fn respond<T: Template>(t: T) -> Response {
463        try_respond(t).unwrap_or_else(|_| {
464            Response::builder()
465                .status(StatusCode::INTERNAL_SERVER_ERROR)
466                .body(Default::default())
467                .unwrap()
468        })
469    }
470
471    impl<'a> From<TemplateFn<'a>> for Response {
472        fn from(slf: TemplateFn<'a>) -> Self {
473            respond(slf)
474        }
475    }
476
477    // impl<'a> TryFrom<TemplateFn<'a>> for Body {
478    //     type Error = Error;
479    //     fn try_from(slf: TemplateFn<'a>) -> Result<Self> {
480    //         slf.render().map(Into::into)
481    //     }
482    // }
483}
484
485#[doc(hidden)]
486#[cfg(feature = "warp")]
487pub mod __warp {
488    pub use warp::reply::{Reply, Response};
489    use warp::{
490        http::{self, header, StatusCode},
491        hyper::Body,
492    };
493
494    use super::*;
495
496    pub fn reply<T: Template>(t: T) -> Response {
497        match t.render() {
498            Ok(body) => http::Response::builder()
499                .status(StatusCode::OK)
500                .header(header::CONTENT_TYPE, HTML_MIME_TYPE)
501                .body(body.into()),
502            Err(_) => http::Response::builder()
503                .status(StatusCode::INTERNAL_SERVER_ERROR)
504                .body(Body::empty()),
505        }
506        .unwrap()
507    }
508
509    impl<'a> Reply for TemplateFn<'a> {
510        fn into_response(self) -> Response {
511            reply(self)
512        }
513    }
514}
515
516#[doc(hidden)]
517#[cfg(feature = "tide")]
518pub mod __tide {
519    pub use tide::{Body, Response};
520
521    use super::*;
522
523    pub fn try_into_body<T: Template>(t: T) -> Result<Body> {
524        let mut body = Body::from_string(t.render()?);
525        body.set_mime(HTML_MIME_TYPE);
526        Ok(body)
527    }
528
529    pub fn into_response<T: Template>(t: T) -> Response {
530        match try_into_body(t) {
531            Ok(body) => {
532                let mut response = Response::new(200);
533                response.set_body(body);
534                response
535            }
536
537            Err(error) => {
538                let mut response = Response::new(500);
539                response.set_error(error);
540                response
541            }
542        }
543    }
544
545    impl<'a> TryFrom<TemplateFn<'a>> for Body {
546        type Error = Error;
547
548        fn try_from(slf: TemplateFn<'a>) -> Result<Self, Self::Error> {
549            try_into_body(slf)
550        }
551    }
552
553    impl<'a> From<TemplateFn<'a>> for Response {
554        fn from(slf: TemplateFn<'a>) -> Self {
555            into_response(slf)
556        }
557    }
558}
559
560#[doc(hidden)]
561#[cfg(feature = "gotham")]
562pub mod __gotham {
563    use gotham::hyper::{
564        self,
565        header::{self, HeaderValue},
566        StatusCode,
567    };
568    pub use gotham::{handler::IntoResponse, state::State};
569
570    use super::*;
571
572    pub type Response<B = hyper::Body> = hyper::Response<B>;
573
574    pub fn respond<T: Template>(t: T) -> Response {
575        match t.render() {
576            Ok(body) => Response::builder()
577                .status(StatusCode::OK)
578                .header(
579                    header::CONTENT_TYPE,
580                    HeaderValue::from_static(HTML_MIME_TYPE),
581                )
582                .body(body.into())
583                .unwrap(),
584            Err(_) => Response::builder()
585                .status(StatusCode::INTERNAL_SERVER_ERROR)
586                .body(vec![].into())
587                .unwrap(),
588        }
589    }
590
591    impl<'a> IntoResponse for TemplateFn<'a> {
592        fn into_response(self, _state: &State) -> Response {
593            respond(self)
594        }
595    }
596}
597
598#[doc(hidden)]
599#[cfg(feature = "rocket")]
600pub mod __rocket {
601    use std::io::Cursor;
602
603    use rocket::{
604        http::{Header, Status},
605        response::Response,
606    };
607    pub use rocket::{
608        response::{Responder, Result},
609        Request,
610    };
611
612    use super::*;
613
614    pub fn respond<T: Template>(t: T) -> Result<'static> {
615        let rsp = t.render().map_err(|_| Status::InternalServerError)?;
616        Response::build()
617            .header(Header::new("content-type", HTML_MIME_TYPE))
618            .sized_body(rsp.len(), Cursor::new(rsp))
619            .ok()
620    }
621
622    impl<'a, 'r, 'o: 'r> Responder<'r, 'o> for TemplateFn<'a> {
623        fn respond_to(self, _req: &'r Request<'_>) -> Result<'o> {
624            respond(self)
625        }
626    }
627}