const HTML_ESCAPE_CHARS: [char; 5] = ['&', '<', '>', '"', '\''];
const HTML_ESCAPE_REPLACEMENTS: [&str; 5] = ["&", "<", ">", """, "'"];
fn escape_html_into(s: &str, output: &mut OutputTarget) {
for segment in s.split_inclusive(|c| HTML_ESCAPE_CHARS.contains(&c)) {
let mut chars = segment.chars();
let Some(last) = chars.next_back() else {
continue;
};
let rest = chars.as_str();
if let Some(index) = HTML_ESCAPE_CHARS.iter().position(|&c| c == last) {
output.push(rest);
output.push(HTML_ESCAPE_REPLACEMENTS[index]);
} else {
output.push(segment);
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum EscapeMode {
None,
Html,
Custom(fn(&str, &mut OutputTarget)),
}
#[derive(Debug, Clone)]
pub enum OutputTarget {
Null,
String(String),
}
impl OutputTarget {
pub fn push(&mut self, s: &str) {
match self {
OutputTarget::Null => {}
OutputTarget::String(o) => o.push_str(s),
}
}
pub fn into_inner(self) -> String {
match self {
OutputTarget::Null => String::new(),
OutputTarget::String(s) => s,
}
}
}
#[derive(Debug, Clone)]
pub struct Output {
target: OutputTarget,
escape: EscapeMode,
}
impl Output {
pub fn null(escape: EscapeMode) -> Self {
Self {
target: OutputTarget::Null,
escape,
}
}
pub fn string(escape: EscapeMode) -> Self {
Self {
target: OutputTarget::String(String::new()),
escape,
}
}
pub fn escape(&self) -> EscapeMode {
self.escape
}
pub fn push(&mut self, s: &str) {
self.target.push(s);
}
pub fn push_escaped(&mut self, s: &str) {
match self.escape {
EscapeMode::None => {
self.target.push(s);
}
EscapeMode::Custom(push_escaped) => {
push_escaped(s, &mut self.target);
}
EscapeMode::Html => {
escape_html_into(s, &mut self.target);
}
}
}
pub fn into_inner(self) -> String {
self.target.into_inner()
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn naive_escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
const MEOW: [char; 4] = ['m', 'e', 'o', 'w'];
fn is_only_meows(s: &str) -> bool {
let mut index = 0;
for c in s.chars() {
if c != MEOW[index] {
return false;
}
index = (index + 1) % MEOW.len();
}
true
}
fn only_meow(s: &str, target: &mut OutputTarget) {
let mut index = 0;
let out: String = s
.chars()
.filter_map(|c| {
if c == MEOW[index] {
index = (index + 1) % MEOW.len();
Some(c)
} else {
None
}
})
.collect();
target.push(&out);
}
proptest! {
#[test]
fn escape_none_works(s in any::<String>()) {
let mut output = Output::string(EscapeMode::None);
output.push_escaped(&s);
assert_eq!(output.into_inner(), s);
}
#[test]
fn escape_html_works(s in any::<String>()) {
let mut output = Output::string(EscapeMode::Html);
output.push_escaped(&s);
assert_eq!(output.into_inner(), naive_escape_html(&s));
}
#[test]
fn escape_custom_works_1(s in any::<String>()) {
let mut output = Output::string(EscapeMode::Custom(only_meow));
output.push_escaped(&s);
assert!(is_only_meows(&output.into_inner()));
}
#[test]
fn escape_custom_works_2(s in "[meow]*") {
let mut output = Output::string(EscapeMode::Custom(only_meow));
output.push_escaped(&s);
assert!(is_only_meows(&output.into_inner()));
}
}
}