pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
pub fn strip_tags(html: &str) -> String {
let mut out = String::with_capacity(html.len());
let mut in_tag = false;
let mut prev_space = true;
for c in html.chars() {
if in_tag {
if c == '>' {
in_tag = false;
}
continue;
}
if c == '<' {
if !prev_space {
out.push(' ');
prev_space = true;
}
in_tag = true;
continue;
}
if c.is_whitespace() {
if !prev_space {
out.push(' ');
prev_space = true;
}
} else {
out.push(c);
prev_space = false;
}
}
if out.ends_with(' ') {
out.pop();
}
out
}
pub fn truncate_words(text: &str, max_chars: usize) -> String {
if max_chars == 0 {
return String::new();
}
if text.chars().count() <= max_chars {
return text.to_string();
}
let budget = max_chars - 1;
let prefix: String = text.chars().take(budget).collect();
let cut = match prefix.rfind(char::is_whitespace) {
Some(i) => prefix[..i].trim_end().to_string(),
None => prefix,
};
format!("{}…", cut)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_all_five() {
assert_eq!(
escape("a & b < c > d \" e ' f"),
"a & b < c > d " e ' f"
);
}
#[test]
fn leaves_plain_text_untouched() {
assert_eq!(escape("hello world"), "hello world");
}
#[test]
fn preserves_non_ascii() {
assert_eq!(escape("café · 你好"), "café · 你好");
}
#[test]
fn empty_string() {
assert_eq!(escape(""), "");
}
#[test]
fn strip_tags_removes_simple_tags() {
assert_eq!(strip_tags("<p>hello <b>world</b></p>"), "hello world");
}
#[test]
fn strip_tags_handles_attributes() {
assert_eq!(
strip_tags(r#"<a href="https://x.test" class="ext">click</a>"#),
"click"
);
}
#[test]
fn strip_tags_collapses_whitespace() {
assert_eq!(strip_tags("a\n\n b c"), "a b c");
}
#[test]
fn strip_tags_trims_edges() {
assert_eq!(strip_tags(" <p>hi</p> "), "hi");
}
#[test]
fn strip_tags_empty_input() {
assert_eq!(strip_tags(""), "");
}
#[test]
fn strip_tags_preserves_entities() {
assert_eq!(
strip_tags("<p>café & tea</p>"),
"café & tea"
);
}
#[test]
fn strip_tags_nested() {
assert_eq!(
strip_tags("<div><p>one</p><p>two <em>three</em></p></div>"),
"one two three"
);
}
#[test]
fn truncate_words_returns_unchanged_when_within_limit() {
assert_eq!(truncate_words("hello world", 250), "hello world");
}
#[test]
fn truncate_words_empty_input() {
assert_eq!(truncate_words("", 250), "");
}
#[test]
fn truncate_words_breaks_on_word_boundary() {
let out = truncate_words("hello world foo bar baz", 15);
assert_eq!(out, "hello world…");
assert!(out.chars().count() <= 15);
}
#[test]
fn truncate_words_hard_cuts_when_no_whitespace() {
let out = truncate_words("abcdefghijklmnop", 8);
assert_eq!(out, "abcdefg…");
assert_eq!(out.chars().count(), 8);
}
#[test]
fn truncate_words_counts_unicode_scalar_values() {
assert_eq!(truncate_words("café", 4), "café");
}
#[test]
fn truncate_words_handles_multibyte_in_truncation() {
let out = truncate_words("你好 世界 abc", 7);
assert_eq!(out, "你好 世界…");
}
#[test]
fn truncate_words_zero_limit() {
assert_eq!(truncate_words("hello", 0), "");
}
}