1use std::{borrow::Cow, collections::HashSet, fmt::Display, io::Write};
2
3extern crate self as html;
4
5fn escape<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
6 let input = input.into();
7 fn needs_escaping(c: char) -> bool {
8 c == '<' || c == '>' || c == '&' || c == '"' || c == '\''
9 }
10
11 if let Some(first) = input.find(needs_escaping) {
12 let mut output = String::from(&input[0..first]);
13 output.reserve(input.len() - first);
14 let rest = input[first..].chars();
15 for c in rest {
16 match c {
17 '<' => output.push_str("<"),
18 '>' => output.push_str(">"),
19 '&' => output.push_str("&"),
20 '"' => output.push_str("""),
21 '\'' => output.push_str("'"),
22 _ => output.push(c),
23 }
24 }
25 Cow::Owned(output)
26 } else {
27 input
28 }
29}
30
31pub struct Element {
32 name: &'static str,
33 attrs: Vec<u8>,
34 children: Option<Box<dyn Render>>,
35 class: String,
36 css: Vec<String>,
37}
38
39macro_rules! impl_attr {
40 ($ident:ident) => {
41 pub fn $ident(self, value: impl Display) -> Self {
42 self.attr(stringify!($ident), value)
43 }
44 };
45
46 ($ident:ident, $name:expr) => {
47 pub fn $ident(self, value: impl Display) -> Self {
48 self.attr($name, value)
49 }
50 };
51}
52
53macro_rules! impl_bool_attr {
54 ($ident:ident) => {
55 pub fn $ident(self) -> Self {
56 self.bool_attr(stringify!($ident))
57 }
58 };
59}
60
61impl Element {
62 fn new(name: &'static str, children: Option<Box<dyn Render>>) -> Element {
63 Element {
64 name,
65 attrs: vec![],
66 children,
67 class: "".into(),
68 css: vec![],
69 }
70 }
71
72 pub fn attr(mut self, name: &'static str, value: impl Display) -> Self {
73 if !self.attrs.is_empty() {
74 self.attrs
75 .write(b" ")
76 .expect("attr failed to write to buffer");
77 }
78 self.attrs
79 .write_fmt(format_args!("{}", name))
80 .expect("attr failed to write to buffer");
81 self.attrs
82 .write(b"=\"")
83 .expect("attr failed to write to buffer");
84 self.attrs
85 .write_fmt(format_args!("{}", escape(value.to_string())))
86 .expect("attr failed to write to buffer");
87 self.attrs
88 .write(b"\"")
89 .expect("attr failed to write to buffer");
90
91 self
92 }
93
94 pub fn bool_attr(mut self, name: &'static str) -> Self {
95 if !self.attrs.is_empty() {
96 self.attrs
97 .write(b" ")
98 .expect("bool_attr failed to write to buffer");
99 }
100 self.attrs
101 .write_fmt(format_args!("{}", name))
102 .expect("bool_attr failed to write to buffer");
103
104 self
105 }
106
107 #[deprecated(since = "0.1.1", note = "Please use type_ instead")]
108 pub fn r#type(self, value: impl Display) -> Self {
109 self.attr("type", value)
110 }
111
112 #[deprecated(since = "0.1.1", note = "Please use for_ instead")]
113 pub fn r#for(self, value: impl Display) -> Self {
114 self.attr("for", value)
115 }
116
117 pub fn css(mut self, value: (impl Display, Vec<&str>)) -> Self {
118 self.css.extend(value.1.into_iter().map(|x| x.to_string()));
119 self.class(value.0)
120 }
121
122 pub fn class(mut self, value: impl Display) -> Self {
123 if self.class.is_empty() {
124 self.class = value.to_string();
125 } else {
126 self.class.push(' ');
127 self.class.push_str(&value.to_string());
128 }
129 self
130 }
131
132 pub fn replace(self, value: impl Display) -> Self {
133 self.attr("x-replace", value)
134 }
135
136 impl_attr!(id);
137 impl_attr!(charset);
138 impl_attr!(content);
139 impl_attr!(name);
140 impl_attr!(href);
141 impl_attr!(rel);
142 impl_attr!(target);
143 impl_attr!(src);
144 impl_attr!(integrity);
145 impl_attr!(crossorigin);
146 impl_attr!(role);
147 impl_attr!(method);
148 impl_attr!(action);
149 impl_attr!(placeholder);
150 impl_attr!(value);
151 impl_attr!(rows);
152 impl_attr!(alt);
153 impl_attr!(style);
154 impl_attr!(onclick);
155 impl_attr!(placement);
156 impl_attr!(toggle);
157 impl_attr!(scope);
158 impl_attr!(title);
159 impl_attr!(lang);
160 impl_attr!(type_, "type");
161 impl_attr!(for_, "for");
162 impl_attr!(aria_controls, "aria-controls");
163 impl_attr!(aria_expanded, "aria-expanded");
164 impl_attr!(aria_label, "aria-label");
165 impl_attr!(aria_haspopup, "aria-haspopup");
166 impl_attr!(aria_labelledby, "aria-labelledby");
167 impl_attr!(aria_current, "aria-current");
168 impl_bool_attr!(defer);
169 impl_bool_attr!(checked);
170 impl_bool_attr!(enabled);
171 impl_bool_attr!(disabled);
172}
173
174pub trait Render {
175 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()>;
176 fn styles(&self, _styles: &mut HashSet<String>) -> std::io::Result<()> {
177 Ok(())
178 }
179}
180
181impl Render for Element {
182 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
183 let name_bytes = self.name.as_bytes();
184 buffer.write(b"<")?;
185 buffer.write(name_bytes)?;
186 if !self.attrs.is_empty() {
187 buffer.write(b" ")?;
188 buffer.write(&self.attrs)?;
189 }
190 if !self.class.is_empty() {
191 buffer.write(b" ")?;
192 buffer.write_fmt(format_args!("class=\"{}\"", self.class))?;
193 }
194 buffer.write(b">")?;
195 match &self.children {
196 Some(children) => {
197 children.render(buffer)?;
198 buffer.write(b"</")?;
199 buffer.write(name_bytes)?;
200 buffer.write(b">")?;
201 }
202 None => {}
203 };
204
205 Ok(())
206 }
207
208 fn styles(&self, styles: &mut HashSet<String>) -> std::io::Result<()> {
209 if !self.css.is_empty() {
210 styles.extend(self.css.clone().into_iter().collect::<HashSet<String>>());
211 }
212 match &self.children {
213 Some(children) => {
214 children.styles(styles)?;
215 }
216 None => {}
217 };
218 Ok(())
219 }
220}
221
222pub struct Raw(pub String);
223
224impl Render for Raw {
225 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
226 buffer.write_fmt(format_args!("{}", self.0))?;
227
228 Ok(())
229 }
230}
231
232pub fn danger(html: impl Display) -> Raw {
233 Raw(html.to_string())
234}
235
236impl Render for String {
237 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
238 buffer.write_fmt(format_args!("{}", escape(self)))?;
239
240 Ok(())
241 }
242}
243
244impl Render for &String {
245 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
246 buffer.write_fmt(format_args!("{}", escape(*self)))?;
247
248 Ok(())
249 }
250}
251
252impl<'a> Render for &'a str {
253 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
254 buffer.write_fmt(format_args!("{}", escape(*self)))?;
255
256 Ok(())
257 }
258}
259
260impl Render for () {
261 fn render(&self, _buffer: &mut Vec<u8>) -> std::io::Result<()> {
262 Ok(())
263 }
264}
265
266impl<T> Render for Vec<T>
267where
268 T: Render,
269{
270 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
271 for t in self {
272 t.render(buffer)?;
273 }
274
275 Ok(())
276 }
277
278 fn styles(&self, buffer: &mut HashSet<String>) -> std::io::Result<()> {
279 for t in self {
280 t.styles(buffer)?;
281 }
282
283 Ok(())
284 }
285}
286
287macro_rules! impl_render_tuple {
288 ($max:expr) => {
289 seq_macro::seq!(N in 0..=$max {
290 impl<#(T~N,)*> Render for (#(T~N,)*)
291 where
292 #(T~N: Render,)*
293 {
294 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
295 #(self.N.render(buffer)?;)*
296
297 Ok(())
298 }
299
300 fn styles(&self, buffer: &mut HashSet<String>) -> std::io::Result<()> {
301 #(self.N.styles(buffer)?;)*
302
303 Ok(())
304 }
305 }
306 });
307 };
308}
309
310seq_macro::seq!(N in 0..=31 {
311 impl_render_tuple!(N);
312});
313
314pub fn doctype() -> Element {
315 Element::new("!DOCTYPE html", None)
316}
317
318pub fn render(renderable: impl Render + 'static) -> String {
319 let mut v: Vec<u8> = vec![];
320 renderable.render(&mut v).expect("Failed to render html");
321 String::from_utf8_lossy(&v).into()
322}
323
324pub fn styles(renderable: &impl Render) -> String {
325 let mut styles: HashSet<String> = HashSet::new();
326 renderable
327 .styles(&mut styles)
328 .expect("Failed to style html");
329 let mut other_styles = styles
330 .iter()
331 .filter(|s| !s.starts_with("@media"))
332 .collect::<Vec<_>>();
333 let media_styles = styles
334 .iter()
335 .filter(|s| s.starts_with("@media"))
336 .collect::<Vec<_>>();
337 other_styles.extend(media_styles);
338 other_styles
339 .into_iter()
340 .map(|s| s.clone())
341 .collect::<Vec<_>>()
342 .join("")
343}
344
345macro_rules! impl_render_num {
346 ($t:ty) => {
347 impl Render for $t {
348 fn render(&self, buffer: &mut Vec<u8>) -> std::io::Result<()> {
349 buffer.write_fmt(format_args!("{}", &self))?;
350 Ok(())
351 }
352 }
353 };
354}
355
356impl_render_num!(u8);
357impl_render_num!(u16);
358impl_render_num!(f64);
359impl_render_num!(f32);
360impl_render_num!(i64);
361impl_render_num!(u64);
362impl_render_num!(i32);
363impl_render_num!(u32);
364impl_render_num!(usize);
365impl_render_num!(isize);
366
367pub fn element(name: &'static str, children: impl Render + 'static) -> Element {
368 Element::new(name, Some(Box::new(children)))
369}
370
371pub fn self_closing_element(name: &'static str) -> Element {
372 Element::new(name, None)
373}
374
375pub fn anon_element(children: impl Render + 'static) -> Element {
376 Element::new("", Some(Box::new(children)))
377}
378
379macro_rules! impl_element {
380 ($ident:ident) => {
381 pub fn $ident(child: impl Render + 'static) -> Element {
382 Element::new(stringify!($ident), Some(Box::new(child)))
383 }
384 };
385}
386
387macro_rules! impl_void_element {
388 ($ident:ident) => {
389 pub fn $ident() -> Element {
390 Element::new(stringify!($ident), None)
391 }
392 };
393}
394
395impl_element!(html);
396impl_element!(head);
397impl_element!(title);
398impl_element!(body);
399impl_element!(div);
400impl_element!(section);
401impl_element!(style);
402impl_element!(h1);
403impl_element!(h2);
404impl_element!(h3);
405impl_element!(h4);
406impl_element!(h5);
407impl_element!(li);
408impl_element!(ul);
409impl_element!(ol);
410impl_element!(p);
411impl_element!(span);
412impl_element!(b);
413impl_element!(i);
414impl_element!(u);
415impl_element!(tt);
416impl_element!(string);
417impl_element!(pre);
418impl_element!(script);
419impl_element!(main);
420impl_element!(nav);
421impl_element!(a);
422impl_element!(form);
423impl_element!(button);
424impl_element!(blockquote);
425impl_element!(footer);
426impl_element!(wrapper);
427impl_element!(label);
428impl_element!(table);
429impl_element!(thead);
430impl_element!(th);
431impl_element!(tr);
432impl_element!(td);
433impl_element!(tbody);
434impl_element!(textarea);
435impl_element!(datalist);
436impl_element!(option);
437
438impl_void_element!(area);
439impl_void_element!(base);
440impl_void_element!(br);
441impl_void_element!(col);
442impl_void_element!(embed);
443impl_void_element!(hr);
444impl_void_element!(img);
445impl_void_element!(input);
446impl_void_element!(link);
447impl_void_element!(meta);
448impl_void_element!(param);
449impl_void_element!(source);
450impl_void_element!(track);
451impl_void_element!(wbr);
452
453#[cfg(test)]
454mod tests {
455 use html::*;
456
457 #[test]
458 fn it_works() {
459 let html = render((doctype(), html((head(()), body(())))));
460 assert_eq!(
461 "<!DOCTYPE html><html><head></head><body></body></html>",
462 html
463 );
464 }
465
466 #[test]
467 fn it_works_with_numbers() {
468 let html = render((doctype(), html((head(()), body(0)))));
469 assert_eq!(
470 "<!DOCTYPE html><html><head></head><body>0</body></html>",
471 html
472 );
473 }
474
475 #[test]
476 fn it_escapes_correctly() {
477 let html = render((doctype(), html((head(()), body("<div />")))));
478 assert_eq!(
479 html,
480 "<!DOCTYPE html><html><head></head><body><div /></body></html>",
481 );
482 }
483
484 #[test]
485 fn it_escapes_more() {
486 let html = render((
487 doctype(),
488 html((head(()), body("<script>alert('hello')</script>"))),
489 ));
490 assert_eq!(
491 html,
492 "<!DOCTYPE html><html><head></head><body><script>alert('hello')</script></body></html>",
493 );
494 }
495
496 #[test]
497 fn it_renders_attributes() {
498 let html = render((doctype(), html((head(()), body(div("hello").id("hello"))))));
499 assert_eq!(
500 "<!DOCTYPE html><html><head></head><body><div id=\"hello\">hello</div></body></html>",
501 html
502 );
503 }
504
505 #[test]
506 fn it_renders_custom_self_closing_elements() {
507 fn hx_close() -> Element {
508 self_closing_element("hx-close")
509 }
510 let html = render(hx_close().id("id"));
511 assert_eq!("<hx-close id=\"id\">", html);
512 }
513
514 #[test]
515 fn readme_works() {
516 use html::*;
517
518 fn render_to_string(element: Element) -> String {
519 render((
520 doctype(),
521 html((
522 head((title("title"), meta().charset("utf-8"))),
523 body(element),
524 )),
525 ))
526 }
527
528 assert_eq!(
529 render_to_string(div("html")),
530 "<!DOCTYPE html><html><head><title>title</title><meta charset=\"utf-8\"></head><body><div>html</div></body></html>"
531 )
532 }
533
534 #[test]
535 fn max_tuples_works() {
536 let elements = seq_macro::seq!(N in 0..=31 {
537 (#(br().id(N),)*)
538 });
539
540 assert_eq!(render(elements),
541 "<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\">"
542 )
543 }
544
545 #[test]
546 fn bool_attr_works() {
547 let html = render(input().type_("checkbox").checked());
548
549 assert_eq!(html, r#"<input type="checkbox" checked>"#)
550 }
551
552 #[test]
553 fn multiple_attrs_spaced_correctly() {
554 let html = render(input().type_("checkbox").checked().aria_label("label"));
555
556 assert_eq!(
557 html,
558 r#"<input type="checkbox" checked aria-label="label">"#
559 )
560 }
561
562 #[test]
563 fn readme1_works() {
564 let element = input()
565 .attr("hx-post", "/")
566 .attr("hx-target", ".target")
567 .attr("hx-swap", "outerHTML")
568 .attr("hx-push-url", "false");
569 let html = render(element);
570
571 assert_eq!(
572 html,
573 r#"<input hx-post="/" hx-target=".target" hx-swap="outerHTML" hx-push-url="false">"#
574 )
575 }
576
577 #[test]
578 fn readme2_works() {
579 fn turbo_frame(children: Element) -> Element {
580 element("turbo-frame", children)
581 }
582 let html = render(turbo_frame(div("inside turbo frame")).id("id"));
583
584 assert_eq!(
585 "<turbo-frame id=\"id\"><div>inside turbo frame</div></turbo-frame>",
586 html
587 );
588 }
589
590 #[test]
591 fn styles_dedup() {
592 let p1 = p("").css((
593 "color-red background-green",
594 vec![
595 ".color-red{color:red;}".into(),
596 ".background-green{background:green;}".into(),
597 ],
598 ));
599 let p2 = p("").css((
600 "color-red background-blue",
601 vec![
602 ".color-red{color:red;}".into(),
603 ".background-blue{background:blue;}".into(),
604 ],
605 ));
606 let div1 = div((p1, p2)).css(("color-red", vec![".color-red{color:red;}".into()]));
607 let styles = styles(&div1);
608 let mut styles = styles
609 .split(".")
610 .into_iter()
611 .filter(|x| !x.is_empty())
612 .map(|x| format!(".{}", x))
613 .collect::<Vec<String>>();
614
615 styles.sort();
616
617 let mut expected: Vec<String> = vec![
618 ".color-red{color:red;}".into(),
619 ".background-blue{background:blue;}".into(),
620 ".background-green{background:green;}".into(),
621 ];
622
623 expected.sort();
624
625 assert_eq!(expected, styles);
626 }
627}