another_html_builder/
attribute.rs

1//! Attribute related module. This contains the traits needed to implement a new
2//! kind of [AttributeValue] but also a wrapper to escape values.
3
4use std::fmt::{Display, Write};
5
6/// Wrapper around a [str] that will escape the content when writing.
7pub struct EscapedValue<'a>(pub &'a str);
8
9impl std::fmt::Display for EscapedValue<'_> {
10    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11        if self.0.is_empty() {
12            return Ok(());
13        }
14        let mut start: usize = 0;
15        while let Some(index) = self.0[start..].find('"') {
16            if index > 0 {
17                f.write_str(&self.0[start..(start + index)])?;
18            }
19            f.write_str("\\\"")?;
20            let end = start + index + 1;
21            debug_assert!(start < end && end <= self.0.len());
22            start = end;
23        }
24        f.write_str(&self.0[start..])?;
25        Ok(())
26    }
27}
28
29macro_rules! attribute_value {
30    ($type:ty) => {
31        impl AttributeValue for $type {
32            fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33                write!(f, "{self}")
34            }
35        }
36    };
37}
38
39/// Represents an element attribute name.
40pub trait AttributeName {
41    fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
42}
43
44impl AttributeName for &str {
45    fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.write_str(self)
47    }
48}
49
50/// Represents an element attribute value.
51///
52/// This value should be escaped for double quotes for example.
53/// The implementation of this trait on `&str` already implements this.
54pub trait AttributeValue {
55    fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
56}
57
58impl AttributeValue for &str {
59    fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        EscapedValue(self).fmt(f)
61    }
62}
63
64#[inline]
65fn render_attr_name_only<N: AttributeName>(
66    f: &mut std::fmt::Formatter<'_>,
67    name: &N,
68) -> std::fmt::Result {
69    f.write_char(' ')?;
70    name.render(f)
71}
72
73#[inline]
74fn render_attr<N: AttributeName, V: AttributeValue>(
75    f: &mut std::fmt::Formatter<'_>,
76    name: &N,
77    value: &V,
78) -> std::fmt::Result {
79    render_attr_name_only(f, name)?;
80    f.write_char('=')?;
81    f.write_char('"')?;
82    value.render(f)?;
83    f.write_char('"')
84}
85
86/// Wrapper used for displaying attributes in elements
87///
88/// This wrapper can print attributes with or without values.
89/// It can also handle attributes wrapped in an `Option` and will behave accordingly.
90///
91/// # Examples
92///
93/// ```rust
94/// let html = another_html_builder::Buffer::default()
95///     .node("div")
96///     .attr("name-only")
97///     .attr(("name", "value"))
98///     .attr(Some(("other", "value")))
99///     .attr(("with-number", 42))
100///     .close()
101///     .into_inner();
102/// assert_eq!(
103///     html,
104///     "<div name-only name=\"value\" other=\"value\" with-number=\"42\" />"
105/// );
106/// ```
107///
108/// # Extending
109///
110/// It's possible to implement attributes with custom types, just by implementing the [AttributeName] and [AttributeValue] traits.
111///
112/// ```rust
113/// use std::fmt::{Display, Write};
114///
115/// struct ClassNames<'a>(&'a [&'static str]);
116///
117/// impl<'a> another_html_builder::attribute::AttributeValue for ClassNames<'a> {
118///     fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119///         for (index, inner) in self.0.iter().enumerate() {
120///             if (index > 0) {
121///                 f.write_char(' ')?;
122///             }
123///             // this could be avoided if you consider it is escaped by default
124///             another_html_builder::attribute::EscapedValue(inner).fmt(f)?;
125///         }
126///         Ok(())
127///     }
128/// }
129///
130/// let html = another_html_builder::Buffer::default()
131///     .node("div")
132///     .attr(("class", ClassNames(&["foo", "bar"])))
133///     .close()
134///     .into_inner();
135/// assert_eq!(html, "<div class=\"foo bar\" />");
136/// ```
137pub struct Attribute<T>(pub T);
138
139impl<N: AttributeName> std::fmt::Display for Attribute<Option<N>> {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        if let Some(ref inner) = self.0 {
142            render_attr_name_only(f, inner)
143        } else {
144            Ok(())
145        }
146    }
147}
148
149impl<N: AttributeName> std::fmt::Display for Attribute<N> {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        render_attr_name_only(f, &self.0)
152    }
153}
154
155impl<N: AttributeName, V: AttributeValue> std::fmt::Display for Attribute<Option<(N, V)>> {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        if let Some((name, value)) = &self.0 {
158            render_attr(f, name, value)
159        } else {
160            Ok(())
161        }
162    }
163}
164
165impl<N: AttributeName, V: AttributeValue> std::fmt::Display for Attribute<(N, V)> {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        let (name, value) = &self.0;
168        render_attr(f, name, value)
169    }
170}
171
172attribute_value!(bool);
173attribute_value!(u8);
174attribute_value!(u16);
175attribute_value!(u32);
176attribute_value!(u64);
177attribute_value!(usize);
178attribute_value!(i8);
179attribute_value!(i16);
180attribute_value!(i32);
181attribute_value!(i64);
182attribute_value!(isize);
183
184#[cfg(test)]
185mod tests {
186    #[test_case::test_case("hello world", "hello world"; "without character to escape")]
187    #[test_case::test_case("a\"b", "a\\\"b"; "with special in the middle")]
188    #[test_case::test_case("\"a", "\\\"a"; "with special at the beginning")]
189    #[test_case::test_case("a\"", "a\\\""; "with special at the end")]
190    fn escaping_attribute(input: &str, expected: &str) {
191        assert_eq!(format!("{}", super::EscapedValue(input)), expected);
192    }
193}