use crate::model::ParseMode;
pub const MARKDOWN_V2_SPECIALS: &[char] =
&['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
pub fn escape_markdown_v2(input: &str) -> String {
let mut out = String::with_capacity(input.len() + input.len() / 4);
for ch in input.chars() {
if ch == '\\' || MARKDOWN_V2_SPECIALS.contains(&ch) {
out.push('\\');
}
out.push(ch);
}
out
}
pub fn escape_html(input: &str) -> String {
let mut out = String::with_capacity(input.len() + input.len() / 8);
for ch in input.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
other => out.push(other),
}
}
out
}
pub fn escape(input: &str, mode: ParseMode) -> String {
match mode {
ParseMode::MarkdownV2 => escape_markdown_v2(input),
ParseMode::Html => escape_html(input),
ParseMode::None => input.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn markdown_v2_escapes_every_special() {
for &c in MARKDOWN_V2_SPECIALS {
let escaped = escape_markdown_v2(&c.to_string());
assert_eq!(escaped, format!("\\{c}"), "char {c:?}");
}
}
#[test]
fn markdown_v2_escapes_backslash_once() {
assert_eq!(escape_markdown_v2("a\\b"), "a\\\\b");
assert_eq!(escape_markdown_v2("\\."), "\\\\\\.");
}
#[test]
fn markdown_v2_leaves_ordinary_text_alone() {
assert_eq!(escape_markdown_v2("feature/добавить emoji ✅"), "feature/добавить emoji ✅");
}
#[test]
fn html_escapes_ampersand_first() {
assert_eq!(escape_html("<b>a & b</b>"), "<b>a & b</b>");
assert_eq!(escape_html("<"), "&lt;");
}
#[test]
fn none_is_identity() {
let s = "*not* escaped <at> all & _fine_";
assert_eq!(escape(s, ParseMode::None), s);
}
#[test]
fn property_no_unescaped_special_survives() {
let corpus = [
"",
"plain",
"a.b_c*d",
"\\\\\\",
"!!!___```",
"русский текст с точкой.",
"emoji 🎉 and (parens)",
"{{.Actor}}",
"-1001234567890",
];
for input in corpus {
let escaped = escape_markdown_v2(input);
let chars: Vec<char> = escaped.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '\\' {
assert!(i + 1 < chars.len(), "dangling backslash in {escaped:?}");
i += 2;
continue;
}
assert!(
!MARKDOWN_V2_SPECIALS.contains(&c),
"unescaped {c:?} in {escaped:?} (input {input:?})"
);
i += 1;
}
}
}
}