1#![cfg_attr(not(feature = "std"), no_std)]
65
66#[cfg(feature = "std")]
67extern crate std;
68
69extern crate alloc;
70
71#[cfg(feature = "typed")]
72pub mod typed;
73
74#[cfg(feature = "macros")]
75pub use html_macro::html;
76
77use alloc::string::{String, ToString};
78use alloc::vec::Vec;
79
80#[derive(Debug, Clone)]
82pub struct Element {
83 tag: String,
84 attrs: Vec<(String, String)>,
85 children: Vec<Node>,
86 self_closing: bool,
87}
88
89#[derive(Debug, Clone)]
91pub enum Node {
92 Element(Element),
93 Text(String),
94 Raw(String),
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct Html {
100 nodes: Vec<Node>,
101}
102
103impl Element {
104 pub fn new(tag: impl Into<String>) -> Self {
106 let tag = tag.into();
107 let self_closing = matches!(
108 tag.as_str(),
109 "area"
110 | "base"
111 | "br"
112 | "col"
113 | "embed"
114 | "hr"
115 | "img"
116 | "input"
117 | "link"
118 | "meta"
119 | "source"
120 | "track"
121 | "wbr"
122 );
123 Element {
124 tag,
125 attrs: Vec::new(),
126 children: Vec::new(),
127 self_closing,
128 }
129 }
130
131 pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
133 self.attrs.push((name.into(), value.into()));
134 self
135 }
136
137 pub fn bool_attr(mut self, name: impl Into<String>) -> Self {
139 self.attrs.push((name.into(), String::new()));
140 self
141 }
142
143 pub fn class(mut self, class: impl Into<String>) -> Self {
145 let class = class.into();
146 if let Some(pos) = self.attrs.iter().position(|(k, _)| k == "class") {
147 self.attrs[pos].1.push(' ');
148 self.attrs[pos].1.push_str(&class);
149 } else {
150 self.attrs.push(("class".to_string(), class));
151 }
152 self
153 }
154
155 pub fn id(self, id: impl Into<String>) -> Self {
157 self.attr("id", id)
158 }
159
160 pub fn text(mut self, content: impl Into<String>) -> Self {
162 self.children.push(Node::Text(content.into()));
163 self
164 }
165
166 pub fn raw(mut self, html: impl Into<String>) -> Self {
168 self.children.push(Node::Raw(html.into()));
169 self
170 }
171
172 pub fn child<F>(mut self, tag: impl Into<String>, f: F) -> Self
174 where
175 F: FnOnce(Element) -> Element,
176 {
177 let child = f(Element::new(tag));
178 self.children.push(Node::Element(child));
179 self
180 }
181
182 pub fn node(mut self, node: Node) -> Self {
184 self.children.push(node);
185 self
186 }
187
188 pub fn children<I, F>(mut self, items: I, f: F) -> Self
190 where
191 I: IntoIterator,
192 F: Fn(I::Item, Element) -> Element,
193 {
194 for item in items {
195 let child = f(item, Element::new(""));
196 if !child.tag.is_empty() {
197 self.children.push(Node::Element(child));
198 }
199 }
200 self
201 }
202
203 pub fn when<F>(self, condition: bool, f: F) -> Self
205 where
206 F: FnOnce(Self) -> Self,
207 {
208 if condition {
209 f(self)
210 } else {
211 self
212 }
213 }
214
215 pub fn when_else<F, G>(self, condition: bool, if_true: F, if_false: G) -> Self
217 where
218 F: FnOnce(Self) -> Self,
219 G: FnOnce(Self) -> Self,
220 {
221 if condition {
222 if_true(self)
223 } else {
224 if_false(self)
225 }
226 }
227
228 pub fn render(&self) -> String {
230 let mut output = String::new();
231 self.render_to(&mut output);
232 output
233 }
234
235 pub fn render_to(&self, output: &mut String) {
237 output.push('<');
238 output.push_str(&self.tag);
239
240 for (name, value) in &self.attrs {
241 output.push(' ');
242 output.push_str(name);
243 if !value.is_empty() {
244 output.push_str("=\"");
245 output.push_str(&escape_attr(value));
246 output.push('"');
247 }
248 }
249
250 if self.self_closing && self.children.is_empty() {
251 output.push_str(" />");
252 } else {
253 output.push('>');
254
255 for child in &self.children {
256 child.render_to(output);
257 }
258
259 output.push_str("</");
260 output.push_str(&self.tag);
261 output.push('>');
262 }
263 }
264}
265
266impl Node {
267 pub fn render(&self) -> String {
269 let mut output = String::new();
270 self.render_to(&mut output);
271 output
272 }
273
274 pub fn render_to(&self, output: &mut String) {
276 match self {
277 Node::Element(elem) => elem.render_to(output),
278 Node::Text(text) => output.push_str(&escape_html(text)),
279 Node::Raw(html) => output.push_str(html),
280 }
281 }
282}
283
284impl Html {
285 pub fn new() -> Self {
287 Html { nodes: Vec::new() }
288 }
289
290 pub fn elem<F>(mut self, tag: impl Into<String>, f: F) -> Self
292 where
293 F: FnOnce(Element) -> Element,
294 {
295 let elem = f(Element::new(tag));
296 self.nodes.push(Node::Element(elem));
297 self
298 }
299
300 pub fn text(mut self, content: impl Into<String>) -> Self {
302 self.nodes.push(Node::Text(content.into()));
303 self
304 }
305
306 pub fn raw(mut self, html: impl Into<String>) -> Self {
308 self.nodes.push(Node::Raw(html.into()));
309 self
310 }
311
312 pub fn build(&self) -> String {
314 let mut output = String::new();
315 for node in &self.nodes {
316 node.render_to(&mut output);
317 }
318 output
319 }
320}
321
322pub fn escape_html(s: &str) -> String {
324 let mut output = String::with_capacity(s.len());
325 for c in s.chars() {
326 match c {
327 '&' => output.push_str("&"),
328 '<' => output.push_str("<"),
329 '>' => output.push_str(">"),
330 _ => output.push(c),
331 }
332 }
333 output
334}
335
336pub fn escape_attr(s: &str) -> String {
338 let mut output = String::with_capacity(s.len());
339 for c in s.chars() {
340 match c {
341 '&' => output.push_str("&"),
342 '<' => output.push_str("<"),
343 '>' => output.push_str(">"),
344 '"' => output.push_str("""),
345 '\'' => output.push_str("'"),
346 _ => output.push(c),
347 }
348 }
349 output
350}
351
352pub fn div<F>(f: F) -> Element
356where
357 F: FnOnce(Element) -> Element,
358{
359 f(Element::new("div"))
360}
361
362pub fn span<F>(f: F) -> Element
364where
365 F: FnOnce(Element) -> Element,
366{
367 f(Element::new("span"))
368}
369
370pub fn table<F>(f: F) -> Element
372where
373 F: FnOnce(Element) -> Element,
374{
375 f(Element::new("table"))
376}
377
378pub fn text_elem(tag: &str, content: impl Into<String>) -> Element {
380 Element::new(tag).text(content)
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use alloc::vec;
387
388 #[test]
389 fn test_simple_element() {
390 let html = Element::new("div")
391 .class("container")
392 .text("Hello")
393 .render();
394 assert_eq!(html, r#"<div class="container">Hello</div>"#);
395 }
396
397 #[test]
398 fn test_nested_elements() {
399 let html = Html::new()
400 .elem("table", |e| {
401 e.class("table").child("tr", |e| {
402 e.child("td", |e| e.text("Cell 1"))
403 .child("td", |e| e.text("Cell 2"))
404 })
405 })
406 .build();
407
408 assert_eq!(
409 html,
410 r#"<table class="table"><tr><td>Cell 1</td><td>Cell 2</td></tr></table>"#
411 );
412 }
413
414 #[test]
415 fn test_attributes() {
416 let html = Element::new("input")
417 .attr("type", "text")
418 .attr("name", "username")
419 .bool_attr("disabled")
420 .render();
421
422 assert_eq!(html, r#"<input type="text" name="username" disabled />"#);
423 }
424
425 #[test]
426 fn test_escape_html() {
427 let html = Element::new("div")
428 .text("<script>alert('xss')</script>")
429 .render();
430 assert_eq!(
431 html,
432 r#"<div><script>alert('xss')</script></div>"#
433 );
434 }
435
436 #[test]
437 fn test_escape_attr() {
438 let html = Element::new("div")
439 .attr("data-value", "say \"hello\"")
440 .render();
441 assert_eq!(html, r#"<div data-value="say "hello""></div>"#);
442 }
443
444 #[test]
445 fn test_class_chaining() {
446 let html = Element::new("div")
447 .class("btn")
448 .class("btn-primary")
449 .class("active")
450 .render();
451 assert_eq!(html, r#"<div class="btn btn-primary active"></div>"#);
452 }
453
454 #[test]
455 fn test_children_iterator() {
456 let items = vec!["Apple", "Banana", "Cherry"];
457 let html = Element::new("ul")
458 .children(items, |item, _| Element::new("li").text(item))
459 .render();
460
461 assert_eq!(
462 html,
463 r#"<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>"#
464 );
465 }
466
467 #[test]
468 fn test_conditional() {
469 let show_button = true;
470 let html = Element::new("div")
471 .when(show_button, |e| e.child("button", |e| e.text("Click me")))
472 .render();
473
474 assert_eq!(html, r#"<div><button>Click me</button></div>"#);
475
476 let show_button = false;
477 let html = Element::new("div")
478 .when(show_button, |e| e.child("button", |e| e.text("Click me")))
479 .render();
480
481 assert_eq!(html, r#"<div></div>"#);
482 }
483
484 #[test]
485 fn test_raw_html() {
486 let html = Element::new("div").raw("<strong>Bold</strong>").render();
487 assert_eq!(html, r#"<div><strong>Bold</strong></div>"#);
488 }
489
490 #[test]
491 fn test_self_closing_tags() {
492 let html = Element::new("br").render();
493 assert_eq!(html, r#"<br />"#);
494
495 let html = Element::new("img").attr("src", "pic.jpg").render();
496 assert_eq!(html, r#"<img src="pic.jpg" />"#);
497 }
498
499 #[test]
500 fn test_address_table_example() {
501 let addresses = vec![(0, "t1abc123", "u1xyz789"), (1, "t1def456", "u1uvw012")];
502
503 let html = Html::new()
504 .elem("table", |t| {
505 t.class("table table-sm")
506 .child("thead", |e| {
507 e.child("tr", |e| {
508 e.child("th", |e| e.text("Index"))
509 .child("th", |e| e.text("Transparent"))
510 .child("th", |e| e.text("Unified"))
511 })
512 })
513 .child("tbody", |e| {
514 e.children(addresses, |(idx, t_addr, u_addr), _| {
515 Element::new("tr")
516 .child("td", |e| e.text(idx.to_string()))
517 .child("td", |e| e.child("code", |e| e.text(t_addr)))
518 .child("td", |e| e.child("code", |e| e.text(u_addr)))
519 })
520 })
521 })
522 .build();
523
524 assert!(html.contains("<table class=\"table table-sm\">"));
525 assert!(html.contains("<code>t1abc123</code>"));
526 assert!(html.contains("<code>u1xyz789</code>"));
527 }
528
529 #[test]
530 fn test_hello_world_page() {
531 let html = Html::new()
532 .raw("<!DOCTYPE html>")
533 .elem("html", |e| {
534 e.attr("lang", "en")
535 .child("head", |e| {
536 e.child("meta", |e| e.attr("charset", "UTF-8"))
537 .child("title", |e| e.text("Hello"))
538 })
539 .child("body", |e| e.child("h1", |e| e.text("Hello, World!")))
540 })
541 .build();
542
543 assert_eq!(
544 html,
545 r#"<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><title>Hello</title></head><body><h1>Hello, World!</h1></body></html>"#
546 );
547 }
548}