another_html_builder/
lib.rs

1//! Just a simple toolkit for writing html.
2//!
3//! This provides the basic functions needed to write basic html or create components to build a rich and complete UI.
4//!
5//! # Example
6//!
7//! In this example, we create a custom attribute and also a custom `Head` element.
8//!
9//! ```rust
10//! use another_html_builder::attribute::AttributeValue;
11//! use another_html_builder::prelude::WriterExt;
12//! use another_html_builder::{Body, Buffer};
13//!
14//! enum Lang {
15//!     En,
16//!     Fr,
17//! }
18//!
19//! impl AttributeValue for Lang {
20//!     fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21//!         f.write_str(match self {
22//!             Self::En => "en",
23//!             Self::Fr => "fr",
24//!         })
25//!     }
26//! }
27//!
28//! struct Head {
29//!     title: &'static str,
30//! }
31//!
32//! impl Default for Head {
33//!     fn default() -> Self {
34//!         Self {
35//!             title: "Hello world!",
36//!         }
37//!     }
38//! }
39//!
40//! impl Head {
41//!     fn render<'a, W: WriterExt>(&self, buf: Buffer<W, Body<'a>>) -> Buffer<W, Body<'a>> {
42//!         buf.node("head")
43//!             .content(|buf| buf.node("title").content(|buf| buf.text(self.title)))
44//!     }
45//! }
46//!
47//! let head = Head::default();
48//! let html = Buffer::default()
49//!     .doctype()
50//!     .node("html")
51//!     .attr(("lang", Lang::Fr))
52//!     .content(|buf| head.render(buf))
53//!     .into_inner();
54//! assert_eq!(
55//!     html,
56//!     "<!DOCTYPE html><html lang=\"fr\"><head><title>Hello world!</title></head></html>"
57//! );
58//! ```
59pub mod attribute;
60pub mod content;
61pub mod prelude;
62
63use crate::prelude::{FmtWriter, IoWriter, WriterExt};
64
65/// Representation of the inside of an element or the root level.
66///
67/// This component is made for the [Buffer] to be aware of where it is
68/// and provide adequat functions.
69#[derive(Debug)]
70pub enum Body<'a> {
71    /// This represents the root of the DOM. It has not name nor parents.
72    Root,
73    /// This represents any element with a name.
74    Element {
75        name: &'a str,
76        parent: Box<Body<'a>>,
77    },
78}
79
80impl Body<'_> {
81    /// Generates the path of the current element.
82    ///
83    /// Note: this will not provid a valide CSS path
84    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/// Representation of an element
98#[derive(Debug)]
99pub struct Element<'a> {
100    parent: Body<'a>,
101    name: &'a str,
102}
103
104/// Wrapper arround a writer element.
105#[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    /// Appends the html doctype to the buffer
155    pub fn doctype(mut self) -> Self {
156        self.inner.write_str("<!DOCTYPE html>").unwrap();
157        self
158    }
159
160    /// Tries to append the html doctype to the buffer
161    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    /// Conditionally apply some children to an element
169    ///
170    /// ```rust
171    /// let is_error = true;
172    /// let html = another_html_builder::Buffer::default()
173    ///     .cond(is_error, |buf| {
174    ///         buf.node("p").content(|buf| buf.text("ERROR!"))
175    ///     })
176    ///     .into_inner();
177    /// assert_eq!(html, "<p>ERROR!</p>");
178    /// ```
179    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    /// Conditionally apply some children to an element depending on an optional
202    ///
203    /// ```rust
204    /// let value: Option<u8> = Some(42);
205    /// let html = another_html_builder::Buffer::default()
206    ///     .optional(value, |buf, answer| {
207    ///         buf.node("p")
208    ///             .content(|buf| buf.text("Answer: ").raw(answer))
209    ///     })
210    ///     .into_inner();
211    /// assert_eq!(html, "<p>Answer: 42</p>");
212    /// ```
213    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    /// Starts a new node in the buffer
240    ///
241    /// After calling this function, the buffer will only allow to add attributes,
242    /// close the current node or add content to the node.
243    ///
244    /// ```rust
245    /// let html = another_html_builder::Buffer::default()
246    ///     .node("p")
247    ///     .attr(("foo", "bar"))
248    ///     .close()
249    ///     .into_inner();
250    /// assert_eq!(html, "<p foo=\"bar\" />");
251    /// ```
252    ///
253    /// ```rust
254    /// let html = another_html_builder::Buffer::default()
255    ///     .node("p")
256    ///     .content(|buf| buf.text("hello"))
257    ///     .into_inner();
258    /// assert_eq!(html, "<p>hello</p>");
259    /// ```
260    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    /// Appends some raw content implementing [Display](std::fmt::Display)
285    ///
286    /// This will not escape the provided value.
287    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    /// Appends some text and escape it.
298    ///
299    /// ```rust
300    /// let html = another_html_builder::Buffer::default()
301    ///     .node("p")
302    ///     .content(|b| b.text("asd\"weiofew!/<>"))
303    ///     .into_inner();
304    /// assert_eq!(html, "<p>asd&quot;weiofew!&#x2F;&lt;&gt;</p>");
305    /// ```
306    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    /// Appends an attribute to the current node.
319    ///
320    /// For more information about how to extend attributes, take a look at the [crate::attribute::Attribute] trait.
321    ///
322    /// ```rust
323    /// let html = another_html_builder::Buffer::default()
324    ///     .node("p")
325    ///     .attr("single")
326    ///     .attr(("hello", "world"))
327    ///     .attr(("number", 42))
328    ///     .attr(Some(("foo", "bar")))
329    ///     .attr(None::<(&str, &str)>)
330    ///     .attr(Some("here"))
331    ///     .attr(None::<&str>)
332    ///     .close()
333    ///     .into_inner();
334    /// assert_eq!(
335    ///     html,
336    ///     "<p single hello=\"world\" number=\"42\" foo=\"bar\" here />"
337    /// );
338    /// ```
339    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    /// Conditionally appends some attributes
357    ///
358    /// ```rust
359    /// let html = another_html_builder::Buffer::default()
360    ///     .node("p")
361    ///     .cond_attr(true, ("foo", "bar"))
362    ///     .cond_attr(false, ("foo", "baz"))
363    ///     .cond_attr(true, "here")
364    ///     .cond_attr(false, "not-here")
365    ///     .close()
366    ///     .into_inner();
367    /// assert_eq!(html, "<p foo=\"bar\" here />");
368    /// ```
369    #[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    /// Closes the current node without providing any content
394    ///
395    /// ```rust
396    /// let html = another_html_builder::Buffer::default()
397    ///     .node("p")
398    ///     .close()
399    ///     .into_inner();
400    /// assert_eq!(html, "<p />");
401    /// ```
402    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    /// Closes the current node and start writing it's content
419    ///
420    /// When returning the inner callback, the closing element will be written to the buffer
421    ///
422    /// ```rust
423    /// let html = another_html_builder::Buffer::default()
424    ///     .node("div")
425    ///     .content(|buf| buf.node("p").close())
426    ///     .into_inner();
427    /// assert_eq!(html, "<div><p /></div>");
428    /// ```
429    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            // This should never happen
453            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            // This should never happen
484            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&quot;weiofew!&#x2F;&lt;&gt;</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}