hers/
lib.rs

1use std::fmt;
2
3pub use hers_macro::hers;
4
5/// A type-safe wrapper for HTML content
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct HtmlString(String);
8
9impl HtmlString {
10    /// Create a new HtmlString from a raw string
11    /// Note: This assumes the string is already properly escaped
12    pub fn new(s: String) -> Self {
13        Self(s)
14    }
15
16    /// Get the inner HTML string
17    pub fn as_str(&self) -> &str {
18        &self.0
19    }
20
21    /// Convert to owned String
22    pub fn into_string(self) -> String {
23        self.0
24    }
25}
26
27impl fmt::Display for HtmlString {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33impl From<String> for HtmlString {
34    fn from(s: String) -> Self {
35        Self::new(s)
36    }
37}
38
39impl From<&str> for HtmlString {
40    fn from(s: &str) -> Self {
41        Self::new(s.to_string())
42    }
43}
44
45/// Trait for types that can be converted to HTML
46pub trait ToHtml {
47    fn to_html(&self) -> HtmlString;
48}
49
50impl ToHtml for String {
51    fn to_html(&self) -> HtmlString {
52        HtmlString::new(escape_html(self))
53    }
54}
55
56impl ToHtml for &str {
57    fn to_html(&self) -> HtmlString {
58        HtmlString::new(escape_html(self))
59    }
60}
61
62impl ToHtml for i32 {
63    fn to_html(&self) -> HtmlString {
64        HtmlString::new(self.to_string())
65    }
66}
67
68impl ToHtml for f64 {
69    fn to_html(&self) -> HtmlString {
70        HtmlString::new(self.to_string())
71    }
72}
73
74impl ToHtml for bool {
75    fn to_html(&self) -> HtmlString {
76        HtmlString::new(self.to_string())
77    }
78}
79
80impl ToHtml for usize {
81    fn to_html(&self) -> HtmlString {
82        HtmlString::new(self.to_string())
83    }
84}
85
86/// Escape HTML special characters to prevent XSS
87pub fn escape_html(input: &str) -> String {
88    input
89        .replace('&', "&amp;")
90        .replace('<', "&lt;")
91        .replace('>', "&gt;")
92        .replace('"', "&quot;")
93        .replace('\'', "&#x27;")
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_html_string_creation() {
102        let html = HtmlString::new("Hello".to_string());
103        assert_eq!(html.as_str(), "Hello");
104        assert_eq!(html.to_string(), "Hello");
105    }
106
107    #[test]
108    fn test_html_string_from_str() {
109        let html: HtmlString = "Hello".into();
110        assert_eq!(html.as_str(), "Hello");
111    }
112
113    #[test]
114    fn test_escape_html() {
115        assert_eq!(escape_html("Hello"), "Hello");
116        assert_eq!(escape_html("<script>"), "&lt;script&gt;");
117        assert_eq!(escape_html("&"), "&amp;");
118        assert_eq!(escape_html("\""), "&quot;");
119        assert_eq!(escape_html("'"), "&#x27;");
120        assert_eq!(
121            escape_html("<script>alert('xss')</script>"),
122            "&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;"
123        );
124    }
125
126    #[test]
127    fn test_to_html_string() {
128        let s = "Hello <world>".to_string();
129        let html = s.to_html();
130        assert_eq!(html.as_str(), "Hello &lt;world&gt;");
131    }
132
133    #[test]
134    fn test_to_html_str() {
135        let html = "Hello <world>".to_html();
136        assert_eq!(html.as_str(), "Hello &lt;world&gt;");
137    }
138
139    #[test]
140    fn test_to_html_numbers() {
141        assert_eq!(42.to_html().as_str(), "42");
142        assert_eq!(7.99.to_html().as_str(), "7.99");
143        assert_eq!(true.to_html().as_str(), "true");
144        assert_eq!(false.to_html().as_str(), "false");
145    }
146}