1use ronami_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.replace('\n', "\n>"))
29}
30
31#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
36 without using its output does nothing useful"]
37pub fn expandable_blockquote(s: &str) -> String {
38 format!("**>{}||", s.replace('\n', "\n>"))
39}
40
41#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
47 without using its output does nothing useful"]
48pub fn italic(s: &str) -> String {
49 if s.starts_with("__") && s.ends_with("__") {
50 format!(r"_{}\r__", &s[..s.len() - 1])
51 } else {
52 format!("_{s}_")
53 }
54}
55
56#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
62 without using its output does nothing useful"]
63pub fn underline(s: &str) -> String {
64 if s.starts_with('_') && s.ends_with('_') { format!(r"__{s}\r__") } else { format!("__{s}__") }
70}
71
72#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
77 without using its output does nothing useful"]
78pub fn strike(s: &str) -> String {
79 format!("~{s}~")
80}
81
82#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
86 without using its output does nothing useful"]
87pub fn link(url: &str, text: &str) -> String {
88 format!("[{}]({})", text, escape_link_url(url))
89}
90
91#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
93 without using its output does nothing useful"]
94pub fn user_mention(user_id: UserId, text: &str) -> String {
95 link(format!("tg://user?id={user_id}").as_str(), text)
96}
97
98#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
102 without using its output does nothing useful"]
103pub fn code_block(code: &str) -> String {
104 format!("```\n{}\n```", escape_code(code))
105}
106
107#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
111 without using its output does nothing useful"]
112pub fn code_block_with_lang(code: &str, lang: &str) -> String {
113 format!("```{}\n{}\n```", escape(lang), escape_code(code))
114}
115
116#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
120 without using its output does nothing useful"]
121pub fn code_inline(s: &str) -> String {
122 format!("`{}`", escape_code(s))
123}
124
125#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
130 without using its output does nothing useful"]
131pub fn escape(s: &str) -> String {
132 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
133 if ESCAPE_CHARS.contains(&c) {
134 s.push('\\');
135 }
136 s.push(c);
137 s
138 })
139}
140
141#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
144 without using its output does nothing useful"]
145pub fn escape_link_url(s: &str) -> String {
146 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
147 if ['`', ')'].contains(&c) {
148 s.push('\\');
149 }
150 s.push(c);
151 s
152 })
153}
154
155#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
158 without using its output does nothing useful"]
159pub fn escape_code(s: &str) -> String {
160 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
161 if ['`', '\\'].contains(&c) {
162 s.push('\\');
163 }
164 s.push(c);
165 s
166 })
167}
168
169#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
170 without using its output does nothing useful"]
171pub fn user_mention_or_link(user: &User) -> String {
172 match user.mention() {
173 Some(mention) => escape(&mention),
174 None => link(user.url().as_str(), &escape(&user.full_name())),
175 }
176}
177
178#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
180 without using its output does nothing useful"]
181pub fn date_time(text: &str, unix_time: i64, format: Option<&str>) -> String {
182 let escaped_text = escape(text);
183 match format {
184 Some(fmt) => format!(""),
185 None => format!(""),
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_bold() {
195 assert_eq!(bold(" foobar "), "* foobar *");
196 assert_eq!(bold(" _foobar_ "), "* _foobar_ *");
197 assert_eq!(bold("~(`foobar`)~"), "*~(`foobar`)~*");
198 }
199
200 #[test]
201 fn test_italic() {
202 assert_eq!(italic(" foobar "), "_ foobar _");
203 assert_eq!(italic("*foobar*"), "_*foobar*_");
204 assert_eq!(italic("~(foobar)~"), "_~(foobar)~_");
205 }
206
207 #[test]
208 fn test_underline() {
209 assert_eq!(underline(" foobar "), "__ foobar __");
210 assert_eq!(underline("*foobar*"), "__*foobar*__");
211 assert_eq!(underline("~(foobar)~"), "__~(foobar)~__");
212 }
213
214 #[test]
215 fn test_strike() {
216 assert_eq!(strike(" foobar "), "~ foobar ~");
217 assert_eq!(strike("*foobar*"), "~*foobar*~");
218 assert_eq!(strike("*(foobar)*"), "~*(foobar)*~");
219 }
220
221 #[test]
222 fn test_italic_with_underline() {
223 assert_eq!(underline(italic("foobar").as_str()), r"___foobar_\r__");
224 assert_eq!(italic(underline("foobar").as_str()), r"___foobar_\r__");
225 }
226
227 #[test]
228 fn test_link() {
229 assert_eq!(
230 link("https://www.google.com/(`foobar`)", "google"),
231 r"[google](https://www.google.com/(\`foobar\`\))",
232 );
233 }
234
235 #[test]
236 fn test_user_mention() {
237 assert_eq!(
238 user_mention(UserId(123_456_789), "pwner666"),
239 "[pwner666](tg://user?id=123456789)"
240 );
241 }
242
243 #[test]
244 fn test_blockquote() {
245 assert_eq!(blockquote("foobar\n\nfoo\nbar"), "**>foobar\n>\n>foo\n>bar");
246 }
247
248 #[test]
249 fn test_expandable_blockquote() {
250 assert_eq!(expandable_blockquote("foobar\n\nfoo\nbar"), "**>foobar\n>\n>foo\n>bar||");
251 }
252
253 #[test]
254 fn test_code_block() {
255 assert_eq!(
256 code_block("pre-'formatted'\nfixed-width \\code `block`"),
257 "```\npre-'formatted'\nfixed-width \\\\code \\`block\\`\n```"
258 );
259 }
260
261 #[test]
262 fn test_code_block_with_lang() {
263 assert_eq!(
264 code_block_with_lang("pre-'formatted'\nfixed-width \\code `block`", "[python]"),
265 "```\\[python\\]\npre-'formatted'\nfixed-width \\\\code \\`block\\`\n```"
266 );
267 }
268
269 #[test]
270 fn test_code_inline() {
271 assert_eq!(code_inline(" let x = (1, 2, 3); "), "` let x = (1, 2, 3); `");
272 assert_eq!(code_inline("<html>foo</html>"), "`<html>foo</html>`");
273 assert_eq!(code_inline(r" `(code inside code \ )` "), r"` \`(code inside code \\ )\` `");
274 }
275
276 #[test]
277 fn test_escape() {
278 assert_eq!(escape("\\!"), r"\\\!");
279 assert_eq!(escape("* foobar *"), r"\* foobar \*");
280 assert_eq!(
281 escape(r"_ * [ ] ( ) ~ \ ` > # + - = | { } . !"),
282 r"\_ \* \[ \] \( \) \~ \\ \` \> \# \+ \- \= \| \{ \} \. \!",
283 );
284 }
285
286 #[test]
287 fn test_escape_link_url() {
288 assert_eq!(
289 escape_link_url(r"https://en.wikipedia.org/wiki/Development+(Software)"),
290 r"https://en.wikipedia.org/wiki/Development+(Software\)"
291 );
292 assert_eq!(
293 escape_link_url(r"https://en.wikipedia.org/wiki/`"),
294 r"https://en.wikipedia.org/wiki/\`"
295 );
296 assert_eq!(escape_link_url(r"_*[]()~`#+-=|{}.!\"), r"_*[](\)~\`#+-=|{}.!\");
297 }
298
299 #[test]
300 fn test_escape_code() {
301 assert_eq!(escape_code(r"` \code inside the code\ `"), r"\` \\code inside the code\\ \`");
302 assert_eq!(escape_code(r"_*[]()~`#+-=|{}.!\"), r"_*[]()~\`#+-=|{}.!\\");
303 }
304
305 #[test]
306 fn user_mention_link() {
307 let user_with_username = User {
308 id: UserId(0),
309 is_bot: false,
310 first_name: "".to_string(),
311 last_name: None,
312 username: Some("abcd".to_string()),
313 language_code: None,
314 is_premium: false,
315 added_to_attachment_menu: false,
316 can_manage_bots: false,
317 supports_guest_queries: false,
318 supports_join_request_queries: false,
319 };
320 assert_eq!(user_mention_or_link(&user_with_username), "@abcd");
321 let user_with_username_underscore =
324 User { username: Some("Abcd_Efg".to_string()), ..user_with_username };
325 assert_eq!(user_mention_or_link(&user_with_username_underscore), "@Abcd\\_Efg");
326 let user_without_username = User {
327 id: UserId(123_456_789),
328 is_bot: false,
329 first_name: "Name".to_string(),
330 last_name: None,
331 username: None,
332 language_code: None,
333 is_premium: false,
334 added_to_attachment_menu: false,
335 can_manage_bots: false,
336 supports_guest_queries: false,
337 supports_join_request_queries: false,
338 };
339 assert_eq!(user_mention_or_link(&user_without_username), "[Name](tg://user/?id=123456789)")
340 }
341}