build_html/
container.rs

1//! This module contains information about containers and container types
2
3use crate::{Html, HtmlContainer, HtmlElement, HtmlTag};
4use std::fmt::{self, Display};
5
6/// The different types of HTML containers that can be added to the page
7#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
8#[non_exhaustive]
9pub enum ContainerType {
10    /// Corresponds to `<address>` tags
11    Address,
12    /// Corresponds to `<article>` tags
13    Article,
14    /// Corresponds to `<div>` tags
15    ///
16    /// This type is also the default for `Container`s
17    #[default]
18    Div,
19    /// Corresponds to `<footer>` tags
20    Footer,
21    /// Corresponds to `<header>` tags
22    Header,
23    /// Corresponds to `<main>` tags
24    Main,
25    /// Corresponds to `<ol>` tags
26    OrderedList,
27    /// Corresponds to `<ul>` tags
28    UnorderedList,
29    /// Corresponts to `<nav>` tags
30    Nav,
31    /// Corresponts to `<section>` tags
32    Section,
33}
34
35impl From<ContainerType> for HtmlTag {
36    fn from(value: ContainerType) -> Self {
37        match value {
38            ContainerType::Address => HtmlTag::Address,
39            ContainerType::Article => HtmlTag::Article,
40            ContainerType::Div => HtmlTag::Div,
41            ContainerType::Footer => HtmlTag::Footer,
42            ContainerType::Header => HtmlTag::Header,
43            ContainerType::Main => HtmlTag::Main,
44            ContainerType::OrderedList => HtmlTag::OrderedList,
45            ContainerType::UnorderedList => HtmlTag::UnorderedList,
46            ContainerType::Nav => HtmlTag::Navigation,
47            ContainerType::Section => HtmlTag::Section,
48        }
49    }
50}
51
52impl Display for ContainerType {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        HtmlTag::from(*self).fmt(f)
55    }
56}
57
58/// A container for HTML elements.
59///
60/// As the name would suggest, a `Container` contains other HTML elements. This struct guarantees
61/// that the elements added will be converted to HTML strings in the same order as they were
62/// added.
63///
64/// Supported container types are provided by the [`ContainerType`] enum.
65///
66/// Note that `Container` elements can be nested inside of each other.
67/// ```rust
68/// # use build_html::*;
69/// let text = Container::new(ContainerType::Main)
70///     .with_header(1, "My Container")
71///     .with_container(
72///         Container::new(ContainerType::Article)
73///             .with_container(
74///                 Container::new(ContainerType::Div)
75///                     .with_paragraph("Inner Text")
76///             )
77///     )
78///     .to_html_string();
79///
80/// assert_eq!(
81///     text,
82///     "<main><h1>My Container</h1><article><div><p>Inner Text</p></div></article></main>"
83/// );
84/// ```
85#[derive(Debug)]
86pub struct Container(HtmlElement);
87
88impl Default for Container {
89    fn default() -> Self {
90        Self::new(Default::default())
91    }
92}
93
94impl Html for Container {
95    fn to_html_string(&self) -> String {
96        self.0.to_html_string()
97    }
98}
99
100impl HtmlContainer for Container {
101    fn add_html<H: Html>(&mut self, content: H) {
102        match self.0.tag {
103            HtmlTag::OrderedList | HtmlTag::UnorderedList => self.0.add_child(
104                HtmlElement::new(HtmlTag::ListElement)
105                    .with_html(content)
106                    .into(),
107            ),
108            _ => self.0.add_html(content),
109        };
110    }
111}
112
113impl Container {
114    /// Creates a new container with the specified tag.
115    pub fn new(tag: ContainerType) -> Self {
116        Self(HtmlElement::new(tag.into()))
117    }
118
119    /// Associates the specified map of attributes with this Container.
120    ///
121    /// Note that this operation overrides all previous `with_attribute` calls on
122    /// this `Container`
123    ///
124    /// # Example
125    /// ```
126    /// # use build_html::*;
127    /// let container = Container::default()
128    ///     .with_attributes(vec![("class", "defaults")])
129    ///     .with_paragraph("text")
130    ///     .to_html_string();
131    ///
132    /// assert_eq!(container, r#"<div class="defaults"><p>text</p></div>"#)
133    /// ```
134    pub fn with_attributes<A, S>(mut self, attributes: A) -> Self
135    where
136        A: IntoIterator<Item = (S, S)>,
137        S: ToString,
138    {
139        for (k, v) in attributes {
140            self.0.add_attribute(k, v);
141        }
142        self
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_content() {
152        // Expected
153        let content = concat!(
154            r#"<h1 id="main-header">header</h1>"#,
155            r#"<img src="myimage.png" alt="test image"/>"#,
156            r#"<a href="rust-lang.org">Rust Home</a>"#,
157            r#"<p class="red-text">Sample Text</p>"#,
158            r#"<pre class="code">Text</pre>"#
159        );
160
161        // Act
162        let sut = Container::new(ContainerType::Article)
163            .with_header_attr(1, "header", [("id", "main-header")])
164            .with_image("myimage.png", "test image")
165            .with_link("rust-lang.org", "Rust Home")
166            .with_paragraph_attr("Sample Text", [("class", "red-text")])
167            .with_preformatted_attr("Text", [("class", "code")]);
168
169        // Assert
170        assert_eq!(
171            sut.to_html_string(),
172            format!(
173                "<{tag}>{content}</{tag}>",
174                tag = ContainerType::Article,
175                content = content
176            )
177        )
178    }
179
180    #[test]
181    fn test_list() {
182        // Expected
183        let content = concat!(
184            r#"<li><h1 id="main-header">header</h1></li>"#,
185            r#"<li><img src="myimage.png" alt="test image"/></li>"#,
186            r#"<li><a href="rust-lang.org">Rust Home</a></li>"#,
187            r#"<li><p class="red-text">Sample Text</p></li>"#,
188            r#"<li><pre class="code">Text</pre></li>"#
189        );
190
191        // Act
192        let sut = Container::new(ContainerType::OrderedList)
193            .with_header_attr(1, "header", [("id", "main-header")])
194            .with_image("myimage.png", "test image")
195            .with_link("rust-lang.org", "Rust Home")
196            .with_paragraph_attr("Sample Text", [("class", "red-text")])
197            .with_preformatted_attr("Text", [("class", "code")]);
198
199        // Assert
200        assert_eq!(
201            sut.to_html_string(),
202            format!(
203                "<{tag}>{content}</{tag}>",
204                tag = ContainerType::OrderedList,
205                content = content
206            )
207        )
208    }
209
210    #[test]
211    fn test_nesting() {
212        // Act
213        let container = Container::new(ContainerType::Main)
214            .with_paragraph("paragraph")
215            .with_container(
216                Container::new(ContainerType::OrderedList)
217                    .with_container(Container::default().with_paragraph(1))
218                    .with_container(Container::default().with_paragraph('2'))
219                    .with_container(Container::default().with_paragraph("3")),
220            )
221            .with_paragraph("done");
222
223        // Assert
224        assert_eq!(
225            container.to_html_string(),
226            concat!(
227                "<main><p>paragraph</p><ol>",
228                "<li><div><p>1</p></div></li>",
229                "<li><div><p>2</p></div></li>",
230                "<li><div><p>3</p></div></li>",
231                "</ol><p>done</p></main>"
232            )
233        )
234    }
235}