1pub mod attribute;
60pub mod content;
61pub mod prelude;
62
63use crate::prelude::{FmtWriter, IoWriter, WriterExt};
64
65#[derive(Debug)]
70pub enum Body<'a> {
71 Root,
73 Element {
75 name: &'a str,
76 parent: Box<Body<'a>>,
77 },
78}
79
80impl Body<'_> {
81 pub fn path(&self) -> String {
85 match self {
86 Self::Root => String::from("$"),
87 Self::Element { name, parent } => {
88 let mut parent_path = parent.path();
89 parent_path.push_str(" > ");
90 parent_path.push_str(name);
91 parent_path
92 }
93 }
94 }
95}
96
97#[derive(Debug)]
99pub struct Element<'a> {
100 parent: Body<'a>,
101 name: &'a str,
102}
103
104#[derive(Clone, Debug)]
106pub struct Buffer<W, C> {
107 inner: W,
108 current: C,
109}
110
111impl Default for Buffer<FmtWriter<String>, Body<'static>> {
112 fn default() -> Self {
113 Self::from(String::new())
114 }
115}
116
117impl<W: std::fmt::Write> From<W> for Buffer<FmtWriter<W>, Body<'static>> {
118 fn from(buffer: W) -> Self {
119 Self {
120 inner: FmtWriter(buffer),
121 current: Body::Root,
122 }
123 }
124}
125
126impl<W: std::io::Write> From<W> for Buffer<IoWriter<W>, Body<'static>> {
127 fn from(value: W) -> Self {
128 Self {
129 inner: IoWriter(value),
130 current: Body::Root,
131 }
132 }
133}
134
135impl<W> Buffer<FmtWriter<W>, Body<'_>> {
136 pub fn into_inner(self) -> W {
137 self.inner.0
138 }
139}
140
141impl<W> Buffer<IoWriter<W>, Body<'_>> {
142 pub fn into_inner(self) -> W {
143 self.inner.0
144 }
145}
146
147impl Buffer<FmtWriter<String>, Body<'_>> {
148 pub fn inner(&self) -> &str {
149 self.inner.0.as_str()
150 }
151}
152
153impl<W: WriterExt> Buffer<W, Body<'_>> {
154 pub fn doctype(mut self) -> Self {
156 self.inner.write_str("<!DOCTYPE html>").unwrap();
157 self
158 }
159
160 pub fn try_doctype(mut self) -> Result<Self, W::Error> {
162 self.inner.write_str("<!DOCTYPE html>")?;
163 Ok(self)
164 }
165}
166
167impl<'a, W: WriterExt> Buffer<W, Body<'a>> {
168 pub fn cond<F>(self, condition: bool, children: F) -> Buffer<W, Body<'a>>
180 where
181 F: FnOnce(Buffer<W, Body>) -> Buffer<W, Body>,
182 {
183 if condition {
184 children(self)
185 } else {
186 self
187 }
188 }
189
190 pub fn try_cond<F>(self, condition: bool, children: F) -> Result<Buffer<W, Body<'a>>, W::Error>
191 where
192 F: FnOnce(Buffer<W, Body>) -> Result<Buffer<W, Body>, W::Error>,
193 {
194 if condition {
195 children(self)
196 } else {
197 Ok(self)
198 }
199 }
200
201 pub fn optional<V, F>(self, value: Option<V>, children: F) -> Buffer<W, Body<'a>>
214 where
215 F: FnOnce(Buffer<W, Body>, V) -> Buffer<W, Body>,
216 {
217 if let Some(inner) = value {
218 children(self, inner)
219 } else {
220 self
221 }
222 }
223
224 pub fn try_optional<V, F>(
225 self,
226 value: Option<V>,
227 children: F,
228 ) -> Result<Buffer<W, Body<'a>>, W::Error>
229 where
230 F: FnOnce(Buffer<W, Body>, V) -> Result<Buffer<W, Body>, W::Error>,
231 {
232 if let Some(inner) = value {
233 children(self, inner)
234 } else {
235 Ok(self)
236 }
237 }
238
239 pub fn node(mut self, tag: &'a str) -> Buffer<W, Element<'a>> {
261 self.inner.write_char('<').unwrap();
262 self.inner.write_str(tag).unwrap();
263 Buffer {
264 inner: self.inner,
265 current: Element {
266 name: tag,
267 parent: self.current,
268 },
269 }
270 }
271
272 pub fn try_node(mut self, tag: &'a str) -> Result<Buffer<W, Element<'a>>, W::Error> {
273 self.inner.write_char('<')?;
274 self.inner.write_str(tag)?;
275 Ok(Buffer {
276 inner: self.inner,
277 current: Element {
278 name: tag,
279 parent: self.current,
280 },
281 })
282 }
283
284 pub fn raw<V: std::fmt::Display>(mut self, value: V) -> Self {
288 self.inner.write(value).unwrap();
289 self
290 }
291
292 pub fn try_raw<V: std::fmt::Display>(mut self, value: V) -> Result<Self, W::Error> {
293 self.inner.write(value)?;
294 Ok(self)
295 }
296
297 pub fn text(mut self, input: &str) -> Self {
307 self.inner.write(content::EscapedContent(input)).unwrap();
308 self
309 }
310
311 pub fn try_text(mut self, input: &str) -> Result<Self, W::Error> {
312 self.inner.write(content::EscapedContent(input))?;
313 Ok(self)
314 }
315}
316
317impl<'a, W: WriterExt> Buffer<W, Element<'a>> {
318 pub fn attr<T>(mut self, attr: T) -> Self
340 where
341 attribute::Attribute<T>: std::fmt::Display,
342 {
343 self.inner.write(attribute::Attribute(attr)).unwrap();
344 self
345 }
346
347 #[inline]
348 pub fn try_attr<T>(mut self, attr: T) -> Result<Self, W::Error>
349 where
350 attribute::Attribute<T>: std::fmt::Display,
351 {
352 self.inner.write(attribute::Attribute(attr))?;
353 Ok(self)
354 }
355
356 #[inline]
370 pub fn cond_attr<T>(self, condition: bool, attr: T) -> Self
371 where
372 attribute::Attribute<T>: std::fmt::Display,
373 {
374 if condition {
375 self.attr(attr)
376 } else {
377 self
378 }
379 }
380
381 #[inline]
382 pub fn try_cond_attr<T>(self, condition: bool, attr: T) -> Result<Self, W::Error>
383 where
384 attribute::Attribute<T>: std::fmt::Display,
385 {
386 if condition {
387 self.try_attr(attr)
388 } else {
389 Ok(self)
390 }
391 }
392
393 pub fn close(mut self) -> Buffer<W, Body<'a>> {
403 self.inner.write_str(" />").unwrap();
404 Buffer {
405 inner: self.inner,
406 current: self.current.parent,
407 }
408 }
409
410 pub fn try_close(mut self) -> Result<Buffer<W, Body<'a>>, W::Error> {
411 self.inner.write_str(" />")?;
412 Ok(Buffer {
413 inner: self.inner,
414 current: self.current.parent,
415 })
416 }
417
418 pub fn content<F>(mut self, children: F) -> Buffer<W, Body<'a>>
430 where
431 F: FnOnce(Buffer<W, Body>) -> Buffer<W, Body>,
432 {
433 self.inner.write_char('>').unwrap();
434 let child_buffer = Buffer {
435 inner: self.inner,
436 current: Body::Element {
437 name: self.current.name,
438 parent: Box::new(self.current.parent),
439 },
440 };
441 let Buffer { mut inner, current } = children(child_buffer);
442 match current {
443 Body::Element { name, parent } => {
444 inner.write_str("</").unwrap();
445 inner.write_str(name).unwrap();
446 inner.write_char('>').unwrap();
447 Buffer {
448 inner,
449 current: *parent,
450 }
451 }
452 Body::Root => Buffer {
454 inner,
455 current: Body::Root,
456 },
457 }
458 }
459
460 pub fn try_content<F>(mut self, children: F) -> Result<Buffer<W, Body<'a>>, W::Error>
461 where
462 F: FnOnce(Buffer<W, Body>) -> Result<Buffer<W, Body>, W::Error>,
463 {
464 self.inner.write_char('>')?;
465 let child_buffer = Buffer {
466 inner: self.inner,
467 current: Body::Element {
468 name: self.current.name,
469 parent: Box::new(self.current.parent),
470 },
471 };
472 let Buffer { mut inner, current } = children(child_buffer)?;
473 match current {
474 Body::Element { name, parent } => {
475 inner.write_str("</")?;
476 inner.write_str(name)?;
477 inner.write_char('>')?;
478 Ok(Buffer {
479 inner,
480 current: *parent,
481 })
482 }
483 Body::Root => Ok(Buffer {
485 inner,
486 current: Body::Root,
487 }),
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use std::io::{Cursor, Write};
495
496 use super::*;
497
498 #[test]
499 fn should_return_inner_value() {
500 let buf = Buffer::default().node("a").content(|buf| buf);
501 assert_eq!(buf.inner(), "<a></a>");
502 }
503
504 #[test]
505 fn should_give_node_path() {
506 let buf = Buffer::default();
507 assert_eq!(buf.current.path(), "$");
508 let _buf = buf.node("a").content(|buf| {
509 assert_eq!(buf.current.path(), "$ > a");
510 buf
511 });
512 }
513
514 #[test]
515 fn should_rollback_after_content() {
516 let buffer = Buffer::default().node("a").content(|buf| buf);
517 assert!(
518 matches!(buffer.current, Body::Root),
519 "found {:?}",
520 buffer.current
521 );
522 }
523
524 #[test]
525 fn simple_html() {
526 let html = Buffer::default()
527 .doctype()
528 .node("html")
529 .attr(("lang", "en"))
530 .content(|buf| {
531 buf.node("head")
532 .content(|buf| {
533 let buf = buf.node("meta").attr(("charset", "utf-8")).close();
534 buf.node("meta")
535 .attr(("name", "viewport"))
536 .attr(("content", "width=device-width, initial-scale=1"))
537 .close()
538 })
539 .node("body")
540 .close()
541 })
542 .into_inner();
543 assert_eq!(
544 html,
545 "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /></head><body /></html>"
546 );
547 }
548
549 #[test]
550 fn with_special_characters_in_attributes() {
551 let html = Buffer::default()
552 .node("a")
553 .attr(("title", "Let's add a quote \" like this"))
554 .attr(("href", "http://example.com?whatever=here"))
555 .content(|b| b.text("Click me!"))
556 .into_inner();
557 assert_eq!(
558 html,
559 "<a title=\"Let's add a quote \\\" like this\" href=\"http://example.com?whatever=here\">Click me!</a>"
560 );
561 }
562
563 #[test]
564 fn with_special_characters_in_content() {
565 let html = Buffer::default()
566 .node("p")
567 .content(|b| b.text("asd\"weiofew!/<>"))
568 .into_inner();
569 assert_eq!(html, "<p>asd"weiofew!/<></p>");
570 }
571
572 #[test]
573 fn with_optional_attributes() {
574 let html = Buffer::default()
575 .node("p")
576 .attr(Some(("foo", "bar")))
577 .attr(None::<(&str, &str)>)
578 .attr(Some("here"))
579 .attr(None::<&str>)
580 .close()
581 .into_inner();
582 assert_eq!(html, "<p foo=\"bar\" here />");
583 }
584
585 #[test]
586 fn with_attributes() {
587 let html = Buffer::default()
588 .node("p")
589 .attr(("foo", "bar"))
590 .attr(("bool", true))
591 .attr(("u8", 42u8))
592 .attr(("i8", -1i8))
593 .close()
594 .into_inner();
595 assert_eq!(html, "<p foo=\"bar\" bool=\"true\" u8=\"42\" i8=\"-1\" />");
596 }
597
598 #[test]
599 fn with_conditional_attributes() {
600 let html = Buffer::default()
601 .node("p")
602 .cond_attr(true, ("foo", "bar"))
603 .cond_attr(false, ("foo", "baz"))
604 .cond_attr(true, "here")
605 .cond_attr(false, "not-here")
606 .close()
607 .into_inner();
608 assert_eq!(html, "<p foo=\"bar\" here />");
609 }
610
611 #[test]
612 fn with_conditional_content() {
613 let notification = false;
614 let connected = true;
615 let html = Buffer::default()
616 .node("div")
617 .content(|buf| {
618 buf.cond(notification, |buf| {
619 buf.node("p")
620 .content(|buf| buf.text("You have a notification"))
621 })
622 .cond(connected, |buf| buf.text("Welcome!"))
623 })
624 .into_inner();
625 assert_eq!(html, "<div>Welcome!</div>");
626 }
627
628 #[test]
629 fn with_optional_content() {
630 let error = Some("This is an error");
631 let html = Buffer::default()
632 .node("div")
633 .content(|buf| buf.optional(error, |buf, msg| buf.text(msg)))
634 .into_inner();
635 assert_eq!(html, "<div>This is an error</div>");
636 }
637
638 #[test]
639 fn should_write_to_io_buffer() {
640 let buf = Buffer::from(Cursor::new(Vec::new()));
641 let buf = buf.node("div").content(|buf| buf.text("Hello World!"));
642 let mut writer = buf.into_inner();
643 writer.flush().unwrap();
644 let inner = writer.into_inner();
645 assert_eq!(&inner, "<div>Hello World!</div>".as_bytes());
646 }
647}