1use teloxide_core::types::{User, UserId};
6
7pub(super) const ESCAPE_CHARS: [char; 19] = [
8 '\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!',
9];
10
11#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
16 without using its output does nothing useful"]
17pub fn bold(s: &str) -> String {
18 format!("*{s}*")
19}
20
21#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
26 without using its output does nothing useful"]
27pub fn blockquote(s: &str) -> String {
28 format!(">{s}")
29}
30
31#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
37 without using its output does nothing useful"]
38pub fn italic(s: &str) -> String {
39 if s.starts_with("__") && s.ends_with("__") {
40 format!(r"_{}\r__", &s[..s.len() - 1])
41 } else {
42 format!("_{s}_")
43 }
44}
45
46#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
52 without using its output does nothing useful"]
53pub fn underline(s: &str) -> String {
54 if s.starts_with('_') && s.ends_with('_') {
60 format!(r"__{s}\r__")
61 } else {
62 format!("__{s}__")
63 }
64}
65
66#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
71 without using its output does nothing useful"]
72pub fn strike(s: &str) -> String {
73 format!("~{s}~")
74}
75
76#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
80 without using its output does nothing useful"]
81pub fn link(url: &str, text: &str) -> String {
82 format!("[{}]({})", text, escape_link_url(url))
83}
84
85#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
87 without using its output does nothing useful"]
88pub fn user_mention(user_id: UserId, text: &str) -> String {
89 link(format!("tg://user?id={user_id}").as_str(), text)
90}
91
92#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
96 without using its output does nothing useful"]
97pub fn code_block(code: &str) -> String {
98 format!("```\n{}\n```", escape_code(code))
99}
100
101#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
105 without using its output does nothing useful"]
106pub fn code_block_with_lang(code: &str, lang: &str) -> String {
107 format!("```{}\n{}\n```", escape(lang), escape_code(code))
108}
109
110#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
114 without using its output does nothing useful"]
115pub fn code_inline(s: &str) -> String {
116 format!("`{}`", escape_code(s))
117}
118
119#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
124 without using its output does nothing useful"]
125pub fn escape(s: &str) -> String {
126 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
127 if ESCAPE_CHARS.contains(&c) {
128 s.push('\\');
129 }
130 s.push(c);
131 s
132 })
133}
134
135#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
138 without using its output does nothing useful"]
139pub fn escape_link_url(s: &str) -> String {
140 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
141 if ['`', ')'].contains(&c) {
142 s.push('\\');
143 }
144 s.push(c);
145 s
146 })
147}
148
149#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
152 without using its output does nothing useful"]
153pub fn escape_code(s: &str) -> String {
154 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
155 if ['`', '\\'].contains(&c) {
156 s.push('\\');
157 }
158 s.push(c);
159 s
160 })
161}
162
163#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
164 without using its output does nothing useful"]
165pub fn user_mention_or_link(user: &User) -> String {
166 match user.mention() {
167 Some(mention) => mention,
168 None => link(user.url().as_str(), &escape(&user.full_name())),
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_bold() {
178 assert_eq!(bold(" foobar "), "* foobar *");
179 assert_eq!(bold(" _foobar_ "), "* _foobar_ *");
180 assert_eq!(bold("~(`foobar`)~"), "*~(`foobar`)~*");
181 }
182
183 #[test]
184 fn test_italic() {
185 assert_eq!(italic(" foobar "), "_ foobar _");
186 assert_eq!(italic("*foobar*"), "_*foobar*_");
187 assert_eq!(italic("~(foobar)~"), "_~(foobar)~_");
188 }
189
190 #[test]
191 fn test_underline() {
192 assert_eq!(underline(" foobar "), "__ foobar __");
193 assert_eq!(underline("*foobar*"), "__*foobar*__");
194 assert_eq!(underline("~(foobar)~"), "__~(foobar)~__");
195 }
196
197 #[test]
198 fn test_strike() {
199 assert_eq!(strike(" foobar "), "~ foobar ~");
200 assert_eq!(strike("*foobar*"), "~*foobar*~");
201 assert_eq!(strike("*(foobar)*"), "~*(foobar)*~");
202 }
203
204 #[test]
205 fn test_italic_with_underline() {
206 assert_eq!(underline(italic("foobar").as_str()), r"___foobar_\r__");
207 assert_eq!(italic(underline("foobar").as_str()), r"___foobar_\r__");
208 }
209
210 #[test]
211 fn test_link() {
212 assert_eq!(
213 link("https://www.google.com/(`foobar`)", "google"),
214 r"[google](https://www.google.com/(\`foobar\`\))",
215 );
216 }
217
218 #[test]
219 fn test_user_mention() {
220 assert_eq!(
221 user_mention(UserId(123_456_789), "pwner666"),
222 "[pwner666](tg://user?id=123456789)"
223 );
224 }
225
226 #[test]
227 fn test_code_block() {
228 assert_eq!(
229 code_block("pre-'formatted'\nfixed-width \\code `block`"),
230 "```\npre-'formatted'\nfixed-width \\\\code \\`block\\`\n```"
231 );
232 }
233
234 #[test]
235 fn test_code_block_with_lang() {
236 assert_eq!(
237 code_block_with_lang("pre-'formatted'\nfixed-width \\code `block`", "[python]"),
238 "```\\[python\\]\npre-'formatted'\nfixed-width \\\\code \\`block\\`\n```"
239 );
240 }
241
242 #[test]
243 fn test_code_inline() {
244 assert_eq!(code_inline(" let x = (1, 2, 3); "), "` let x = (1, 2, 3); `");
245 assert_eq!(code_inline("<html>foo</html>"), "`<html>foo</html>`");
246 assert_eq!(code_inline(r" `(code inside code \ )` "), r"` \`(code inside code \\ )\` `");
247 }
248
249 #[test]
250 fn test_escape() {
251 assert_eq!(escape("\\!"), r"\\\!");
252 assert_eq!(escape("* foobar *"), r"\* foobar \*");
253 assert_eq!(
254 escape(r"_ * [ ] ( ) ~ \ ` > # + - = | { } . !"),
255 r"\_ \* \[ \] \( \) \~ \\ \` \> \# \+ \- \= \| \{ \} \. \!",
256 );
257 }
258
259 #[test]
260 fn test_escape_link_url() {
261 assert_eq!(
262 escape_link_url(r"https://en.wikipedia.org/wiki/Development+(Software)"),
263 r"https://en.wikipedia.org/wiki/Development+(Software\)"
264 );
265 assert_eq!(
266 escape_link_url(r"https://en.wikipedia.org/wiki/`"),
267 r"https://en.wikipedia.org/wiki/\`"
268 );
269 assert_eq!(escape_link_url(r"_*[]()~`#+-=|{}.!\"), r"_*[](\)~\`#+-=|{}.!\");
270 }
271
272 #[test]
273 fn test_escape_code() {
274 assert_eq!(escape_code(r"` \code inside the code\ `"), r"\` \\code inside the code\\ \`");
275 assert_eq!(escape_code(r"_*[]()~`#+-=|{}.!\"), r"_*[]()~\`#+-=|{}.!\\");
276 }
277
278 #[test]
279 fn user_mention_link() {
280 let user_with_username = User {
281 id: UserId(0),
282 is_bot: false,
283 first_name: "".to_string(),
284 last_name: None,
285 username: Some("abcd".to_string()),
286 language_code: None,
287 is_premium: false,
288 added_to_attachment_menu: false,
289 };
290 assert_eq!(user_mention_or_link(&user_with_username), "@abcd");
291 let user_without_username = User {
292 id: UserId(123_456_789),
293 is_bot: false,
294 first_name: "Name".to_string(),
295 last_name: None,
296 username: None,
297 language_code: None,
298 is_premium: false,
299 added_to_attachment_menu: false,
300 };
301 assert_eq!(user_mention_or_link(&user_without_username), "[Name](tg://user/?id=123456789)")
302 }
303}