Skip to main content

privatus_analytics/
html.rs

1use std::fmt;
2
3/// HTML that is already escaped and safe to print as it is.
4///
5/// Every helper in this crate returns `SafeHtml`. Printing it with `{}` writes
6/// the HTML without escaping it again. With the `askama` feature it is not
7/// escaped by askama, and with the `maud` feature it implements
8/// [`maud::Render`](https://docs.rs/maud/latest/maud/trait.Render.html).
9#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
10pub struct SafeHtml(String);
11
12impl SafeHtml {
13    /// Wraps a string you know is safe HTML. Nothing is escaped.
14    pub fn from_trusted(html: impl Into<String>) -> Self {
15        SafeHtml(html.into())
16    }
17
18    /// The HTML as a string slice.
19    pub fn as_str(&self) -> &str {
20        &self.0
21    }
22
23    /// The HTML as an owned string.
24    pub fn into_string(self) -> String {
25        self.0
26    }
27
28    /// Whether there is nothing to print.
29    pub fn is_empty(&self) -> bool {
30        self.0.is_empty()
31    }
32}
33
34impl fmt::Display for SafeHtml {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.write_str(&self.0)
37    }
38}
39
40impl AsRef<str> for SafeHtml {
41    fn as_ref(&self) -> &str {
42        &self.0
43    }
44}
45
46impl From<SafeHtml> for String {
47    fn from(html: SafeHtml) -> String {
48        html.0
49    }
50}
51
52/// Escapes text for an HTML attribute value or element body.
53///
54/// `&`, `<`, `>`, `"` and `'` become `&amp;`, `&lt;`, `&gt;`, `&quot;` and `&#39;`.
55pub fn escape_html(text: &str) -> String {
56    let mut out = String::with_capacity(text.len());
57    for ch in text.chars() {
58        match ch {
59            '&' => out.push_str("&amp;"),
60            '<' => out.push_str("&lt;"),
61            '>' => out.push_str("&gt;"),
62            '"' => out.push_str("&quot;"),
63            '\'' => out.push_str("&#39;"),
64            _ => out.push(ch),
65        }
66    }
67    out
68}
69
70/// Cuts a string to at most `max` characters (not bytes).
71pub(crate) fn truncate_chars(text: &str, max: usize) -> &str {
72    match text.char_indices().nth(max) {
73        Some((index, _)) => &text[..index],
74        None => text,
75    }
76}