telegram_rs/
html.rs

1//! Utils for working with the [HTML message style][spec].
2//!
3//! [spec]: https://core.telegram.org/bots/api#html-style
4
5use teloxide_core::types::{User, UserId};
6
7/// Applies the bold font style to the string.
8///
9/// Passed string will not be automatically escaped because it can contain
10/// nested markup.
11#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
12              without using its output does nothing useful"]
13pub fn bold(s: &str) -> String {
14    format!("<b>{s}</b>")
15}
16
17/// Applies the block quotation style to the string.
18///
19/// Passed string will not be automatically escaped because it can contain
20/// nested markup.
21#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
22              without using its output does nothing useful"]
23pub fn blockquote(s: &str) -> String {
24    format!("<blockquote>{s}</blockquote>")
25}
26
27/// Applies the italic font style to the string.
28///
29/// Passed string will not be automatically escaped because it can contain
30/// nested markup.
31#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
32              without using its output does nothing useful"]
33pub fn italic(s: &str) -> String {
34    format!("<i>{s}</i>")
35}
36
37/// Applies the underline font style to the string.
38///
39/// Passed string will not be automatically escaped because it can contain
40/// nested markup.
41#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
42              without using its output does nothing useful"]
43pub fn underline(s: &str) -> String {
44    format!("<u>{s}</u>")
45}
46
47/// Applies the strikethrough font style to the string.
48///
49/// Passed string will not be automatically escaped because it can contain
50/// nested markup.
51#[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 strike(s: &str) -> String {
54    format!("<s>{s}</s>")
55}
56
57/// Builds an inline link with an anchor.
58///
59/// Escapes the passed URL and the link text.
60#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
61              without using its output does nothing useful"]
62pub fn link(url: &str, text: &str) -> String {
63    format!("<a href=\"{}\">{}</a>", escape(url), escape(text))
64}
65
66/// Builds an inline user mention link with an anchor.
67#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
68              without using its output does nothing useful"]
69pub fn user_mention(user_id: UserId, text: &str) -> String {
70    link(format!("tg://user?id={user_id}").as_str(), text)
71}
72
73/// Formats the code block.
74///
75/// Escapes HTML characters inside the block.
76#[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 code_block(code: &str) -> String {
79    format!("<pre>{}</pre>", escape(code))
80}
81
82/// Formats the code block with a specific language syntax.
83///
84/// Escapes HTML characters inside the block.
85#[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 code_block_with_lang(code: &str, lang: &str) -> String {
88    format!(
89        "<pre><code class=\"language-{}\">{}</code></pre>",
90        escape(lang).replace('"', "&quot;"),
91        escape(code)
92    )
93}
94
95/// Formats the string as an inline code.
96///
97/// Escapes HTML characters inside the block.
98#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
99              without using its output does nothing useful"]
100pub fn code_inline(s: &str) -> String {
101    format!("<code>{}</code>", escape(s))
102}
103
104/// Escapes the string to be shown "as is" within the Telegram HTML message
105/// style.
106///
107/// Does not escape ' and " characters (as should be for usual HTML), because
108/// they shouldn't be escaped by the [spec].
109///
110/// [spec]: https://core.telegram.org/bots/api#html-style
111#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
112              without using its output does nothing useful"]
113pub fn escape(s: &str) -> String {
114    s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
115        match c {
116            '&' => s.push_str("&amp;"),
117            '<' => s.push_str("&lt;"),
118            '>' => s.push_str("&gt;"),
119            c => s.push(c),
120        }
121        s
122    })
123}
124
125#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
126              without using its output does nothing useful"]
127pub fn user_mention_or_link(user: &User) -> String {
128    match user.mention() {
129        Some(mention) => mention,
130        None => link(user.url().as_str(), &user.full_name()),
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_bold() {
140        assert_eq!(bold(" foobar "), "<b> foobar </b>");
141        assert_eq!(bold(" <i>foobar</i> "), "<b> <i>foobar</i> </b>");
142        assert_eq!(bold("<s>(`foobar`)</s>"), "<b><s>(`foobar`)</s></b>");
143    }
144
145    #[test]
146    fn test_italic() {
147        assert_eq!(italic(" foobar "), "<i> foobar </i>");
148        assert_eq!(italic(" <b>foobar</b> "), "<i> <b>foobar</b> </i>");
149        assert_eq!(italic("<s>(`foobar`)</s>"), "<i><s>(`foobar`)</s></i>");
150    }
151
152    #[test]
153    fn test_underline() {
154        assert_eq!(underline(" foobar "), "<u> foobar </u>");
155        assert_eq!(underline(" <b>foobar</b> "), "<u> <b>foobar</b> </u>");
156        assert_eq!(underline("<s>(`foobar`)</s>"), "<u><s>(`foobar`)</s></u>");
157    }
158
159    #[test]
160    fn test_strike() {
161        assert_eq!(strike(" foobar "), "<s> foobar </s>");
162        assert_eq!(strike(" <b>foobar</b> "), "<s> <b>foobar</b> </s>");
163        assert_eq!(strike("<b>(`foobar`)</b>"), "<s><b>(`foobar`)</b></s>");
164    }
165
166    #[test]
167    fn test_link() {
168        assert_eq!(
169            link("https://www.google.com/?q=foo&l=ru", "<google>"),
170            "<a href=\"https://www.google.com/?q=foo&amp;l=ru\">&lt;google&gt;</a>",
171        );
172    }
173
174    #[test]
175    fn test_user_mention() {
176        assert_eq!(
177            user_mention(UserId(123_456_789), "<pwner666>"),
178            "<a href=\"tg://user?id=123456789\">&lt;pwner666&gt;</a>",
179        );
180    }
181
182    #[test]
183    fn test_code_block() {
184        assert_eq!(
185            code_block("<p>pre-'formatted'\n & fixed-width \\code `block`</p>"),
186            "<pre>&lt;p&gt;pre-'formatted'\n &amp; fixed-width \\code `block`&lt;/p&gt;</pre>"
187        );
188    }
189
190    #[test]
191    fn test_code_block_with_lang() {
192        assert_eq!(
193            code_block_with_lang(
194                "<p>pre-'formatted'\n & fixed-width \\code `block`</p>",
195                "<html>\"",
196            ),
197            concat!(
198                "<pre><code class=\"language-&lt;html&gt;&quot;\">",
199                "&lt;p&gt;pre-'formatted'\n &amp; fixed-width \\code `block`&lt;/p&gt;",
200                "</code></pre>",
201            )
202        );
203    }
204
205    #[test]
206    fn test_code_inline() {
207        assert_eq!(
208            code_inline("<span class=\"foo\">foo & bar</span>"),
209            "<code>&lt;span class=\"foo\"&gt;foo &amp; bar&lt;/span&gt;</code>",
210        );
211    }
212
213    #[test]
214    fn test_escape() {
215        assert_eq!(
216            escape("  <title>Foo & Bar</title>   "),
217            "  &lt;title&gt;Foo &amp; Bar&lt;/title&gt;   "
218        );
219        assert_eq!(escape("<p>你好 & 再見</p>"), "&lt;p&gt;你好 &amp; 再見&lt;/p&gt;");
220        assert_eq!(escape("'foo\""), "'foo\"");
221    }
222
223    #[test]
224    fn user_mention_link() {
225        let user_with_username = User {
226            id: UserId(0),
227            is_bot: false,
228            first_name: "".to_string(),
229            last_name: None,
230            username: Some("abcd".to_string()),
231            language_code: None,
232            is_premium: false,
233            added_to_attachment_menu: false,
234        };
235        assert_eq!(user_mention_or_link(&user_with_username), "@abcd");
236        let user_without_username = User {
237            id: UserId(123_456_789),
238            is_bot: false,
239            first_name: "Name".to_string(),
240            last_name: None,
241            username: None,
242            language_code: None,
243            is_premium: false,
244            added_to_attachment_menu: false,
245        };
246        assert_eq!(
247            user_mention_or_link(&user_without_username),
248            r#"<a href="tg://user/?id=123456789">Name</a>"#
249        )
250    }
251}