html_escaper/
lib.rs

1use core::fmt::{self, Display, Formatter, Write};
2
3pub trait Escape {
4  /// Write `self` to `f`, escaping if necessary, and appending a newline if
5  /// `newline`.
6  fn escape(&self, f: &mut Formatter, newline: bool) -> fmt::Result;
7}
8
9/// Disable escaping for the wrapped value.
10pub struct Trusted<T: Display>(pub T);
11
12impl<T: Display> Escape for T {
13  fn escape(&self, f: &mut Formatter, newline: bool) -> fmt::Result {
14    if newline {
15      writeln!(HtmlEscaper(f), "{}", self)
16    } else {
17      write!(HtmlEscaper(f), "{}", self)
18    }
19  }
20}
21
22impl<T: Display> Escape for Trusted<T> {
23  fn escape(&self, f: &mut Formatter, newline: bool) -> fmt::Result {
24    if newline {
25      writeln!(f, "{}", self.0)
26    } else {
27      write!(f, "{}", self.0)
28    }
29  }
30}
31
32/// Escaping wrapper for `core::fmt::Formatter`
33pub struct HtmlEscaper<'a, 'b>(pub &'a mut Formatter<'b>);
34
35impl Write for HtmlEscaper<'_, '_> {
36  fn write_str(&mut self, s: &str) -> core::fmt::Result {
37    let mut i = 0;
38    for (j, c) in s.char_indices() {
39      let replacement = match c {
40        '"' => Some("&quot;"),
41        '&' => Some("&amp;"),
42        '<' => Some("&lt;"),
43        '>' => Some("&gt;"),
44        '\'' => Some("&apos;"),
45        _ => None,
46      };
47      if let Some(replacement) = replacement {
48        if i < j {
49          self.0.write_str(&s[i..j])?;
50        }
51        self.0.write_str(replacement)?;
52        i = j + c.len_utf8();
53      }
54    }
55
56    if i < s.len() {
57      self.0.write_str(&s[i..])?;
58    }
59
60    Ok(())
61  }
62}
63
64#[cfg(test)]
65mod tests {
66  use {
67    super::*,
68    core::fmt::{self, Display},
69  };
70
71  struct Wrapper(&'static str);
72
73  impl Display for Wrapper {
74    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
75      write!(HtmlEscaper(f), "{}", self.0)
76    }
77  }
78
79  #[test]
80  fn unescaped_characters() {
81    assert_eq!(Wrapper("hello").to_string(), "hello");
82  }
83
84  #[test]
85  fn escaped_characters() {
86    assert_eq!(Wrapper("\"").to_string(), "&quot;");
87    assert_eq!(Wrapper("&").to_string(), "&amp;");
88    assert_eq!(Wrapper("'").to_string(), "&apos;");
89    assert_eq!(Wrapper("<").to_string(), "&lt;");
90    assert_eq!(Wrapper(">").to_string(), "&gt;");
91  }
92
93  #[test]
94  fn mixed_characters() {
95    assert_eq!(Wrapper("foo&bar&baz").to_string(), "foo&amp;bar&amp;baz");
96  }
97}