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("<"),
17 '>' => output.push_str(">"),
18 '&' => output.push_str("&"),
19 '"' => output.push_str("""),
20 '\'' => output.push_str("'"),
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><div /></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><script>alert('hello')</script></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}