use std::borrow::Cow;
#[inline]
const fn is_misc_escape(b: u8) -> bool {
matches!(
b,
b'\\' | b'&' | b'<' | b'`' | b'[' | b']' | b'>' | b'~' | b'#' | b'=' | b'+' | b'|' | b'-'
)
}
#[inline]
const fn is_ascii_punct(b: u8) -> bool {
matches!(
b,
b'!' | b'"'
| b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b'-'
| b'.'
| b'/'
| b':'
| b';'
| b'<'
| b'='
| b'>'
| b'?'
| b'@'
| b'['
| b'\\'
| b']'
| b'^'
| b'_'
| b'`'
| b'{'
| b'|'
| b'}'
| b'~'
)
}
#[allow(clippy::fn_params_excessive_bools)]
pub fn escape_into(
dest: &mut String,
text: &str,
escape_misc: bool,
escape_asterisks: bool,
escape_underscores: bool,
escape_ascii: bool,
) {
if text.is_empty() {
return;
}
if escape_ascii {
escape_ascii_into(dest, text);
return;
}
let bytes = text.as_bytes();
let mut run_start = 0;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
let needs_misc = escape_misc && is_misc_escape(b);
let needs_numbered = escape_misc && (b == b'.' || b == b')') && i > 0 && bytes[i - 1].is_ascii_digit();
let needs_star = escape_asterisks && b == b'*';
let needs_under = escape_underscores && b == b'_';
if needs_misc || needs_numbered || needs_star || needs_under {
if i > run_start {
dest.push_str(&text[run_start..i]);
}
dest.push('\\');
dest.push(b as char);
i += 1;
run_start = i;
} else {
i += 1;
}
}
if i > run_start {
dest.push_str(&text[run_start..]);
}
}
fn escape_ascii_into(dest: &mut String, text: &str) {
let bytes = text.as_bytes();
let mut run_start = 0;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if is_ascii_punct(b) {
if i > run_start {
dest.push_str(&text[run_start..i]);
}
dest.push('\\');
dest.push(b as char);
i += 1;
run_start = i;
} else {
i += 1;
}
}
if i > run_start {
dest.push_str(&text[run_start..]);
}
}
#[allow(clippy::fn_params_excessive_bools)]
pub fn escape(
text: &str,
escape_misc: bool,
escape_asterisks: bool,
escape_underscores: bool,
escape_ascii: bool,
) -> Cow<'_, str> {
if text.is_empty() {
return Cow::Borrowed("");
}
if !escape_misc && !escape_asterisks && !escape_underscores && !escape_ascii {
return Cow::Borrowed(text);
}
let needs_any = text.as_bytes().iter().any(|&b| {
if escape_ascii {
return is_ascii_punct(b);
}
(escape_misc && (is_misc_escape(b) || b == b'.' || b == b')'))
|| (escape_asterisks && b == b'*')
|| (escape_underscores && b == b'_')
});
if !needs_any {
return Cow::Borrowed(text);
}
let mut dest = String::with_capacity(text.len() + 8);
escape_into(
&mut dest,
text,
escape_misc,
escape_asterisks,
escape_underscores,
escape_ascii,
);
Cow::Owned(dest)
}
#[must_use]
pub fn chomp(text: &str) -> (&str, &str, &str) {
if text.is_empty() {
return ("", "", "");
}
let prefix = if text.starts_with(|c: char| c.is_whitespace()) {
" "
} else {
""
};
let suffix = if text.ends_with("\n\n") || text.ends_with("\r\n\r\n") {
"\n\n"
} else if text.ends_with([' ', '\t']) {
" "
} else {
""
};
let trimmed = if suffix == "\n\n" {
text.trim_end_matches("\n\n").trim_end_matches("\r\n\r\n").trim()
} else {
text.trim()
};
(prefix, suffix, trimmed)
}
#[must_use]
pub fn normalize_whitespace(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut prev_was_space = false;
for ch in text.chars() {
let is_space = ch == ' ' || ch == '\t' || is_unicode_space(ch);
if is_space {
if !prev_was_space {
result.push(' ');
prev_was_space = true;
}
} else {
result.push(ch);
prev_was_space = false;
}
}
result
}
#[must_use]
pub fn normalize_whitespace_cow(text: &str) -> Cow<'_, str> {
let bytes = text.as_bytes();
let mut prev_was_space = false;
for &b in bytes {
if b >= 0x80 {
return normalize_whitespace_cow_slow(text);
}
let is_space = b == b' ' || b == b'\t';
if is_space {
if prev_was_space || b != b' ' {
return Cow::Owned(normalize_whitespace(text));
}
prev_was_space = true;
} else {
prev_was_space = false;
}
}
Cow::Borrowed(text)
}
#[cold]
fn normalize_whitespace_cow_slow(text: &str) -> Cow<'_, str> {
let mut prev_was_space = false;
for ch in text.chars() {
let is_space = ch == ' ' || ch == '\t' || is_unicode_space(ch);
if is_space {
if prev_was_space || ch != ' ' {
return Cow::Owned(normalize_whitespace(text));
}
prev_was_space = true;
} else {
prev_was_space = false;
}
}
Cow::Borrowed(text)
}
#[must_use]
pub fn decode_html_entities(text: &str) -> String {
html_escape::decode_html_entities(text).into_owned()
}
#[must_use]
pub fn decode_html_entities_cow(text: &str) -> Cow<'_, str> {
if !text.contains('&') {
return Cow::Borrowed(text);
}
html_escape::decode_html_entities(text)
}
const fn is_unicode_space(ch: char) -> bool {
matches!(
ch,
'\u{00A0}'
| '\u{1680}'
| '\u{2000}'
| '\u{2001}'
| '\u{2002}'
| '\u{2003}'
| '\u{2004}'
| '\u{2005}'
| '\u{2006}'
| '\u{2007}'
| '\u{2008}'
| '\u{2009}'
| '\u{200A}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_misc() {
assert_eq!(escape("foo & bar", true, false, false, false), r"foo \& bar");
assert_eq!(escape("foo [bar]", true, false, false, false), r"foo \[bar\]");
assert_eq!(escape("1. Item", true, false, false, false), r"1\. Item");
assert_eq!(escape("1) Item", true, false, false, false), r"1\) Item");
}
#[test]
fn test_escape_asterisks() {
assert_eq!(escape("foo * bar", false, true, false, false), r"foo \* bar");
assert_eq!(escape("**bold**", false, true, false, false), r"\*\*bold\*\*");
}
#[test]
fn test_escape_underscores() {
assert_eq!(escape("foo_bar", false, false, true, false), r"foo\_bar");
assert_eq!(escape("__bold__", false, false, true, false), r"\_\_bold\_\_");
}
#[test]
fn test_escape_ascii() {
assert_eq!(escape(r##"!"#$%&"##, false, false, false, true), r#"\!\"\#\$\%\&"#);
assert_eq!(escape("*+,-./", false, false, false, true), r"\*\+\,\-\.\/");
assert_eq!(escape("<=>?@", false, false, false, true), r"\<\=\>\?\@");
assert_eq!(escape(r"[\]^_`", false, false, false, true), r"\[\\\]\^\_\`");
assert_eq!(escape("{|}~", false, false, false, true), r"\{\|\}\~");
}
#[test]
fn test_chomp() {
assert_eq!(chomp(" text "), (" ", " ", "text"));
assert_eq!(chomp("text"), ("", "", "text"));
assert_eq!(chomp(" text"), (" ", "", "text"));
assert_eq!(chomp("text "), ("", " ", "text"));
assert_eq!(chomp(""), ("", "", ""));
}
}