hyped/
lib.rs

1use std::{borrow::Cow, fmt::Display, io::Write};
2extern crate self as hyped;
3
4fn escape<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
5    let input = input.into();
6    fn needs_escaping(c: char) -> bool {
7        c == '<' || c == '>' || c == '&' || c == '"' || c == '\''
8    }
9
10    if let Some(first) = input.find(needs_escaping) {
11        let mut output = String::from(&input[0..first]);
12        output.reserve(input.len() - first);
13        let rest = input[first..].chars();
14        for c in rest {
15            match c {
16                '<' => output.push_str("&lt;"),
17                '>' => output.push_str("&gt;"),
18                '&' => output.push_str("&amp;"),
19                '"' => output.push_str("&quot;"),
20                '\'' => output.push_str("&#39;"),
21                _ => output.push(c),
22            }
23        }
24        Cow::Owned(output)
25    } else {
26        input
27    }
28}
29
30pub struct Element {
31    name: &'static str,
32    attrs: Vec<u8>,
33    children: Option<Box<dyn Render>>,
34}
35
36macro_rules! impl_attr {
37    ($ident:ident) => {
38        pub fn $ident(self, value: impl Display) -> Self {
39            self.attr(stringify!($ident), value)
40        }
41    };
42
43    ($ident:ident, $name:expr) => {
44        pub fn $ident(self, value: impl Display) -> Self {
45            self.attr($name, value)
46        }
47    };
48}
49
50macro_rules! impl_bool_attr {
51    ($ident:ident) => {
52        pub fn $ident(self) -> Self {
53            self.bool_attr(stringify!($ident))
54        }
55    };
56}
57
58impl Element {
59    fn new(name: &'static str, children: Option<Box<dyn Render>>) -> Element {
60        Element {
61            name,
62            attrs: vec![],
63            children,
64        }
65    }
66
67    pub fn attr(mut self, name: &'static str, value: impl Display) -> Self {
68        if !self.attrs.is_empty() {
69            self.attrs
70                .write(b" ")
71                .expect("attr failed to write to buffer");
72        }
73        self.attrs
74            .write_fmt(format_args!("{}", name))
75            .expect("attr failed to write to buffer");
76        self.attrs
77            .write(b"=\"")
78            .expect("attr failed to write to buffer");
79        self.attrs
80            .write_fmt(format_args!("{}", escape(value.to_string())))
81            .expect("attr failed to write to buffer");
82        self.attrs
83            .write(b"\"")
84            .expect("attr failed to write to buffer");
85
86        self
87    }
88
89    pub fn bool_attr(mut self, name: &'static str) -> Self {
90        if !self.attrs.is_empty() {
91            self.attrs
92                .write(b" ")
93                .expect("bool_attr failed to write to buffer");
94        }
95        self.attrs
96            .write_fmt(format_args!("{}", name))
97            .expect("bool_attr failed to write to buffer");
98
99        self
100    }
101
102    impl_attr!(class);
103    impl_attr!(id);
104    impl_attr!(charset);
105    impl_attr!(content);
106    impl_attr!(name);
107    impl_attr!(href);
108    impl_attr!(rel);
109    impl_attr!(target);
110    impl_attr!(src);
111    impl_attr!(integrity);
112    impl_attr!(crossorigin);
113    impl_attr!(role);
114    impl_attr!(method);
115    impl_attr!(action);
116    impl_attr!(placeholder);
117    impl_attr!(value);
118    impl_attr!(rows);
119    impl_attr!(alt);
120    impl_attr!(style);
121    impl_attr!(onclick);
122    impl_attr!(placement);
123    impl_attr!(toggle);
124    impl_attr!(scope);
125    impl_attr!(title);
126    impl_attr!(lang);
127    impl_attr!(r#type, "type");
128    impl_attr!(r#for, "for");
129    impl_attr!(aria_controls, "aria-controls");
130    impl_attr!(aria_expanded, "aria-expanded");
131    impl_attr!(aria_label, "aria-label");
132    impl_attr!(aria_haspopup, "aria-haspopup");
133    impl_attr!(aria_labelledby, "aria-labelledby");
134    impl_attr!(aria_current, "aria-current");
135    impl_bool_attr!(defer);
136    impl_bool_attr!(checked);
137    impl_bool_attr!(enabled);
138    impl_bool_attr!(disabled);
139}
140
141pub trait Render {
142    fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()>;
143}
144
145impl Render for Element {
146    fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
147        let name_bytes = self.name.as_bytes();
148        buffer.write(b"<")?;
149        buffer.write(name_bytes)?;
150        if !self.attrs.is_empty() {
151            buffer.write(b" ")?;
152            buffer.write(&self.attrs)?;
153        }
154        buffer.write(b">")?;
155        match &self.children {
156            Some(children) => {
157                children.render(buffer)?;
158                buffer.write(b"</")?;
159                buffer.write(name_bytes)?;
160                buffer.write(b">")?;
161            }
162            None => {}
163        };
164
165        Ok(())
166    }
167}
168
169pub struct Raw(String);
170
171impl Render for Raw {
172    fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
173        buffer.write_fmt(format_args!("{}", self.0))?;
174
175        Ok(())
176    }
177}
178
179pub fn danger(html: impl Display) -> Raw {
180    Raw(html.to_string())
181}
182
183impl Render for String {
184    fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
185        buffer.write_fmt(format_args!("{}", escape(self)))?;
186
187        Ok(())
188    }
189}
190
191impl<'a> Render for &'a str {
192    fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
193        buffer.write_fmt(format_args!("{}", escape(*self)))?;
194
195        Ok(())
196    }
197}
198
199impl Render for () {
200    fn render(&self, _buffer: &mut Vec<u8>) -> std::io::Result<()> {
201        Ok(())
202    }
203}
204
205impl<T> Render for Vec<T>
206where
207    T: Render,
208{
209    fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
210        for t in self {
211            t.render(buffer)?;
212        }
213
214        Ok(())
215    }
216}
217
218macro_rules! impl_render_tuple {
219    ($max:expr) => {
220        seq_macro::seq!(N in 0..=$max {
221            impl<#(T~N,)*> Render for (#(T~N,)*)
222            where
223                #(T~N: Render,)*
224            {
225                fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
226                    #(self.N.render(buffer)?;)*
227
228                    Ok(())
229                }
230            }
231        });
232    };
233}
234
235seq_macro::seq!(N in 0..=31 {
236    impl_render_tuple!(N);
237});
238
239pub fn doctype() -> Element {
240    Element::new("!DOCTYPE html", None)
241}
242
243pub fn render(renderable: impl Render + 'static) -> String {
244    let mut v: Vec<u8> = vec![];
245    renderable.render(&mut v).expect("Failed to render html");
246    String::from_utf8_lossy(&v).into()
247}
248
249macro_rules! impl_render_num {
250    ($t:ty) => {
251        impl Render for $t {
252            fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
253                buffer.write_fmt(format_args!("{}", &self))?;
254                Ok(())
255            }
256        }
257    };
258}
259
260impl_render_num!(u8);
261impl_render_num!(u16);
262impl_render_num!(f64);
263impl_render_num!(f32);
264impl_render_num!(i64);
265impl_render_num!(u64);
266impl_render_num!(i32);
267impl_render_num!(u32);
268impl_render_num!(usize);
269impl_render_num!(isize);
270
271pub fn element(name: &'static str, children: impl Render + 'static) -> Element {
272    Element::new(name, Some(Box::new(children)))
273}
274
275pub fn self_closing_element(name: &'static str) -> Element {
276    Element::new(name, None)
277}
278
279macro_rules! impl_element {
280    ($ident:ident) => {
281        pub fn $ident(child: impl Render + 'static) -> Element {
282            Element::new(stringify!($ident), Some(Box::new(child)))
283        }
284    };
285}
286
287macro_rules! impl_self_closing_element {
288    ($ident:ident) => {
289        pub fn $ident() -> Element {
290            Element::new(stringify!($ident), None)
291        }
292    };
293}
294
295impl_element!(html);
296impl_element!(head);
297impl_element!(title);
298impl_element!(body);
299impl_element!(div);
300impl_element!(section);
301impl_element!(h1);
302impl_element!(h2);
303impl_element!(h3);
304impl_element!(h4);
305impl_element!(h5);
306impl_element!(li);
307impl_element!(ul);
308impl_element!(ol);
309impl_element!(p);
310impl_element!(span);
311impl_element!(b);
312impl_element!(i);
313impl_element!(u);
314impl_element!(tt);
315impl_element!(string);
316impl_element!(pre);
317impl_element!(script);
318impl_element!(main);
319impl_element!(nav);
320impl_element!(a);
321impl_element!(form);
322impl_element!(button);
323impl_element!(blockquote);
324impl_element!(footer);
325impl_element!(wrapper);
326impl_element!(label);
327impl_element!(table);
328impl_element!(thead);
329impl_element!(th);
330impl_element!(tr);
331impl_element!(td);
332impl_element!(tbody);
333impl_element!(textarea);
334impl_element!(datalist);
335impl_element!(option);
336impl_element!(link);
337
338impl_self_closing_element!(input);
339impl_self_closing_element!(meta);
340impl_self_closing_element!(img);
341impl_self_closing_element!(br);
342
343#[cfg(test)]
344mod tests {
345    use hyped::*;
346
347    #[test]
348    fn it_works() {
349        let html = render((doctype(), html((head(()), body(())))));
350        assert_eq!(
351            "<!DOCTYPE html><html><head></head><body></body></html>",
352            html
353        );
354    }
355
356    #[test]
357    fn it_works_with_numbers() {
358        let html = render((doctype(), html((head(()), body(0)))));
359        assert_eq!(
360            "<!DOCTYPE html><html><head></head><body>0</body></html>",
361            html
362        );
363    }
364
365    #[test]
366    fn it_escapes_correctly() {
367        let html = render((doctype(), html((head(()), body("<div />")))));
368        assert_eq!(
369            html,
370            "<!DOCTYPE html><html><head></head><body>&lt;div /&gt;</body></html>",
371        );
372    }
373
374    #[test]
375    fn it_escapes_more() {
376        let html = render((
377            doctype(),
378            html((head(()), body("<script>alert('hello')</script>"))),
379        ));
380        assert_eq!(
381            html,
382            "<!DOCTYPE html><html><head></head><body>&lt;script&gt;alert(&#39;hello&#39;)&lt;/script&gt;</body></html>",
383        );
384    }
385
386    #[test]
387    fn it_renders_attributes() {
388        let html = render((doctype(), html((head(()), body(div("hello").id("hello"))))));
389        assert_eq!(
390            "<!DOCTYPE html><html><head></head><body><div id=\"hello\">hello</div></body></html>",
391            html
392        );
393    }
394
395    #[test]
396    fn it_renders_custom_self_closing_elements() {
397        fn hx_close() -> Element {
398            self_closing_element("hx-close")
399        }
400        let html = render(hx_close().id("id"));
401        assert_eq!("<hx-close id=\"id\">", html);
402    }
403
404    #[test]
405    fn readme_works() {
406        use hyped::*;
407
408        fn render_to_string(element: Element) -> String {
409            render((
410                doctype(),
411                html((
412                    head((title("title"), meta().charset("utf-8"))),
413                    body(element),
414                )),
415            ))
416        }
417
418        assert_eq!(
419        render_to_string(div("hyped")),
420        "<!DOCTYPE html><html><head><title>title</title><meta charset=\"utf-8\"></head><body><div>hyped</div></body></html>"
421      )
422    }
423
424    #[test]
425    fn max_tuples_works() {
426        let elements = seq_macro::seq!(N in 0..=31 {
427            (#(br().id(N),)*)
428        });
429
430        assert_eq!(render(elements),
431            "<br id=\"0\"><br id=\"1\"><br id=\"2\"><br id=\"3\"><br id=\"4\"><br id=\"5\"><br id=\"6\"><br id=\"7\"><br id=\"8\"><br id=\"9\"><br id=\"10\"><br id=\"11\"><br id=\"12\"><br id=\"13\"><br id=\"14\"><br id=\"15\"><br id=\"16\"><br id=\"17\"><br id=\"18\"><br id=\"19\"><br id=\"20\"><br id=\"21\"><br id=\"22\"><br id=\"23\"><br id=\"24\"><br id=\"25\"><br id=\"26\"><br id=\"27\"><br id=\"28\"><br id=\"29\"><br id=\"30\"><br id=\"31\">"
432        )
433    }
434
435    #[test]
436    fn bool_attr_works() {
437        let html = render(input().r#type("checkbox").checked());
438
439        assert_eq!(html, r#"<input type="checkbox" checked>"#)
440    }
441
442    #[test]
443    fn multiple_attrs_spaced_correctly() {
444        let html = render(input().r#type("checkbox").checked().aria_label("label"));
445
446        assert_eq!(
447            html,
448            r#"<input type="checkbox" checked aria-label="label">"#
449        )
450    }
451
452    #[test]
453    fn readme1_works() {
454        let element = input()
455            .attr("hx-post", "/")
456            .attr("hx-target", ".target")
457            .attr("hx-swap", "outerHTML")
458            .attr("hx-push-url", "false");
459        let html = render(element);
460
461        assert_eq!(
462            html,
463            r#"<input hx-post="/" hx-target=".target" hx-swap="outerHTML" hx-push-url="false">"#
464        )
465    }
466
467    #[test]
468    fn readme2_works() {
469        fn turbo_frame(children: Element) -> Element {
470            element("turbo-frame", children)
471        }
472        let html = render(turbo_frame(div("inside turbo frame")).id("id"));
473
474        assert_eq!(
475            "<turbo-frame id=\"id\"><div>inside turbo frame</div></turbo-frame>",
476            html
477        );
478    }
479}