pub trait HtmlEscape {
fn escape_html(&self) -> String;
}
pub struct RawHtml(pub String);
impl RawHtml {
pub fn new<S: Into<String>>(s: S) -> Self {
RawHtml(s.into())
}
}
impl HtmlEscape for RawHtml {
fn escape_html(&self) -> String {
self.0.clone()
}
}
impl HtmlEscape for String {
fn escape_html(&self) -> String {
escape_str(self)
}
}
impl HtmlEscape for &str {
fn escape_html(&self) -> String {
escape_str(self)
}
}
impl<T: HtmlEscape + ?Sized> HtmlEscape for &T {
fn escape_html(&self) -> String {
(*self).escape_html()
}
}
macro_rules! impl_safe_primitives {
($($t:ty),*) => {
$(
impl HtmlEscape for $t {
fn escape_html(&self) -> String {
self.to_string()
}
}
)*
};
}
impl_safe_primitives!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool
);
pub fn escape_str(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for c in s.chars() {
match c {
'<' => escaped.push_str("<"),
'>' => escaped.push_str(">"),
'&' => escaped.push_str("&"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
_ => escaped.push(c),
}
}
escaped
}
pub fn escape<T: HtmlEscape + ?Sized>(val: &T) -> String {
val.escape_html()
}
pub fn escape_attr<T: HtmlEscape + ?Sized>(val: &T) -> String {
val.escape_html()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_escape_complex_javascript_types() {
let js = r#"<script>let a = {"b": 1, c: '2', d: [1,2,3]}; alert(a);</script>"#;
let expected = "<script>let a = {"b": 1, c: '2', d: [1,2,3]}; alert(a);</script>";
assert_eq!(escape_str(js), expected);
}
#[test]
fn test_escape_str_edge_cases() {
assert_eq!(escape_str(""), "");
assert_eq!(escape_str("Café & croissant"), "Café & croissant");
assert_eq!(escape_str("<script>"), "<script>");
assert_eq!(escape_str("\"'"), ""'");
}
#[test]
fn test_raw_html_new() {
let raw = RawHtml::new("<b>bold</b>");
assert_eq!(raw.0, "<b>bold</b>");
}
}