vkteams_bot/api/utils/
parser.rs

1use crate::api::types::*;
2use crate::error::{BotError, Result};
3use reqwest::Url;
4use std::convert::From;
5const HTML_LIST_ITEM_OVERHEAD: usize = 10; // <li></li> is 7 characters long
6pub trait MessageTextHTMLParser {
7    /// Create new parser
8    fn new() -> Self
9    where
10        Self: Sized + Default,
11    {
12        Self::default()
13    }
14    /// Add formatted text to parser
15    fn add(&mut self, text: MessageTextFormat) -> Self;
16    /// Add new row to parser
17    fn next_line(&mut self) -> Self;
18    /// Add space to parser
19    fn space(&mut self) -> Self;
20    /// Parse text to HTML
21    fn parse(&self) -> Result<(String, ParseMode)>;
22}
23impl MessageTextParser {
24    /// Parse [`MessageTextFormat`] types to HTML string
25    fn parse_html(&self, text: &MessageTextFormat) -> Result<String> {
26        match text {
27            MessageTextFormat::Plain(text) => Ok(self.replace_chars(text)),
28            MessageTextFormat::Link(url, text) => {
29                let parsed_url = Url::parse(&self.replace_chars(url))?;
30                Ok(format!(
31                    "<a href=\"{}\">{}</a>",
32                    parsed_url,
33                    self.replace_chars(text)
34                ))
35            }
36            MessageTextFormat::Bold(text) => Ok(format!("<b>{}</b>", self.replace_chars(text))),
37            MessageTextFormat::Italic(text) => Ok(format!("<i>{}</i>", self.replace_chars(text))),
38            MessageTextFormat::Code(text) => {
39                Ok(format!("<code>{}</code>", self.replace_chars(text)))
40            }
41            MessageTextFormat::Pre(text, class) => match class {
42                Some(class) => Ok(format!(
43                    "<pre class=\"{}\">{}</pre>",
44                    self.replace_chars(class),
45                    self.replace_chars(text)
46                )),
47                None => Ok(format!("<pre>{}</pre>", self.replace_chars(text))),
48            },
49            MessageTextFormat::Mention(chat_id) => Ok(format!("<a>@[{chat_id}]</a>")),
50            MessageTextFormat::Strikethrough(text) => {
51                Ok(format!("<s>{}</s>", self.replace_chars(text)))
52            }
53            MessageTextFormat::Underline(text) => {
54                Ok(format!("<u>{}</u>", self.replace_chars(text)))
55            }
56            MessageTextFormat::Quote(text) => Ok(format!(
57                "<blockquote>{}</blockquote>",
58                self.replace_chars(text)
59            )),
60            MessageTextFormat::OrderedList(list) => {
61                let estimated_size = list.iter().map(|s| s.len() + HTML_LIST_ITEM_OVERHEAD).sum();
62                let mut result = String::with_capacity(estimated_size);
63                for item in list {
64                    result.push_str(&format!("<li>{}</li>", self.replace_chars(item)));
65                }
66                Ok(format!("<ol>{result}</ol>"))
67            }
68            MessageTextFormat::UnOrderedList(list) => {
69                let estimated_size = list.iter().map(|s| s.len() + 10).sum();
70                let mut result = String::with_capacity(estimated_size);
71                for item in list {
72                    result.push_str(&format!("<li>{}</li>", self.replace_chars(item)));
73                }
74                Ok(format!("<ul>{result}</ul>"))
75            }
76            MessageTextFormat::None => Err(BotError::Validation(
77                "MessageTextFormat::None is not supported".to_string(),
78            )),
79        }
80    }
81    /// Replace special characters with HTML entities
82    fn replace_chars(&self, text: &str) -> String {
83        text.replace('&', "&amp;")
84            .replace('<', "&lt;")
85            .replace('>', "&gt;")
86    }
87}
88impl MessageTextHTMLParser for MessageTextParser {
89    /// Add plain text to [`MessageTextFormat`]
90    /// ## Parameters
91    /// - `text`: [`String`] - Text
92    fn add(&mut self, text: MessageTextFormat) -> Self {
93        self.text.push(text);
94        self.to_owned()
95    }
96    /// Line feed
97    fn next_line(&mut self) -> Self {
98        self.text.push(MessageTextFormat::Plain(String::from("\n")));
99        self.to_owned()
100    }
101    /// Space
102    fn space(&mut self) -> Self {
103        self.text.push(MessageTextFormat::Plain(String::from(" ")));
104        self.to_owned()
105    }
106    /// Parse [`MessageTextFormat`] to string
107    fn parse(&self) -> Result<(String, ParseMode)> {
108        let mut result = String::new();
109        match self.parse_mode {
110            ParseMode::HTML => {
111                for item in &self.text {
112                    if let MessageTextFormat::None = item {
113                        continue;
114                    }
115                    result.push_str(&self.parse_html(item)?);
116                }
117                Ok((result, self.parse_mode))
118            }
119            #[cfg(feature = "templates")]
120            ParseMode::Template => {
121                result.push_str(self.parse_tmpl()?.as_str());
122                Ok((result, ParseMode::HTML))
123            }
124            ParseMode::MarkdownV2 => {
125                // MarkdownV2 is not supported in this parser
126                Err(BotError::Validation(format!(
127                    "Parse mode not supported: {:?}. Supported modes: HTML, Template",
128                    self.parse_mode
129                )))
130            }
131        }
132    }
133}
134pub use crate::api::types::MessageTextParser;
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::api::types::{ChatId, MessageTextFormat, ParseMode};
140
141    fn parser_html() -> MessageTextParser {
142        MessageTextParser {
143            text: vec![],
144            parse_mode: ParseMode::HTML,
145            ..Default::default()
146        }
147    }
148
149    #[test]
150    fn test_plain_text() {
151        let mut parser = parser_html();
152        parser = parser.add(MessageTextFormat::Plain("Hello".to_string()));
153        let (html, mode) = parser.parse().unwrap();
154        assert_eq!(html, "Hello");
155        assert_eq!(mode, ParseMode::HTML);
156    }
157
158    #[test]
159    fn test_bold_italic_code() {
160        let mut parser = parser_html();
161        parser = parser.add(MessageTextFormat::Bold("B".to_string()));
162        parser = parser.add(MessageTextFormat::Italic("I".to_string()));
163        parser = parser.add(MessageTextFormat::Code("C".to_string()));
164        let (html, _) = parser.parse().unwrap();
165        assert!(html.contains("<b>B</b>"));
166        assert!(html.contains("<i>I</i>"));
167        assert!(html.contains("<code>C</code>"));
168    }
169
170    #[test]
171    fn test_pre_with_and_without_class() {
172        let mut parser = parser_html();
173        parser = parser.add(MessageTextFormat::Pre(
174            "code".to_string(),
175            Some("lang".to_string()),
176        ));
177        parser = parser.add(MessageTextFormat::Pre("code2".to_string(), None));
178        let (html, _) = parser.parse().unwrap();
179        assert!(html.contains("<pre class=\"lang\">code</pre>"));
180        assert!(html.contains("<pre>code2</pre>"));
181    }
182
183    #[test]
184    fn test_link_and_mention() {
185        let mut parser = parser_html();
186        parser = parser.add(MessageTextFormat::Link(
187            "http://a.com".to_string(),
188            "A".to_string(),
189        ));
190        parser = parser.add(MessageTextFormat::Mention(ChatId::from("cid")));
191        let (html, _) = parser.parse().unwrap();
192        // println!("HTML output: {}", html);
193        assert!(html.contains("<a href=\"http://a.com/\">A</a>"));
194        assert!(html.contains("<a>@[cid]</a>"));
195    }
196
197    #[test]
198    fn test_strikethrough_underline_quote() {
199        let mut parser = parser_html();
200        parser = parser.add(MessageTextFormat::Strikethrough("S".to_string()));
201        parser = parser.add(MessageTextFormat::Underline("U".to_string()));
202        parser = parser.add(MessageTextFormat::Quote("Q".to_string()));
203        let (html, _) = parser.parse().unwrap();
204        assert!(html.contains("<s>S</s>"));
205        assert!(html.contains("<u>U</u>"));
206        assert!(html.contains("<blockquote>Q</blockquote>"));
207    }
208
209    #[test]
210    fn test_ordered_and_unordered_list() {
211        let mut parser = parser_html();
212        parser = parser.add(MessageTextFormat::OrderedList(vec![
213            "A".to_string(),
214            "B".to_string(),
215        ]));
216        parser = parser.add(MessageTextFormat::UnOrderedList(vec!["X".to_string()]));
217        let (html, _) = parser.parse().unwrap();
218        assert!(html.contains("<ol><li>A</li><li>B</li></ol>"));
219        assert!(html.contains("<ul><li>X</li></ul>"));
220    }
221
222    #[test]
223    fn test_none_format_returns_error() {
224        let mut parser = parser_html();
225        parser = parser.add(MessageTextFormat::None);
226        let res = parser.parse();
227        assert!(res.is_ok()); // None should be ignored, not error
228    }
229
230    #[test]
231    fn test_replace_chars_html_escape() {
232        let parser = parser_html();
233        let s = parser.replace_chars("<tag>&text>");
234        assert_eq!(s, "&lt;tag&gt;&amp;text&gt;");
235    }
236
237    #[test]
238    fn test_next_line_and_space() {
239        let mut parser = parser_html();
240        parser = parser.add(MessageTextFormat::Plain("A".to_string()));
241        parser = parser.space();
242        parser = parser.add(MessageTextFormat::Plain("B".to_string()));
243        parser = parser.next_line();
244        parser = parser.add(MessageTextFormat::Plain("C".to_string()));
245        let (html, _) = parser.parse().unwrap();
246        assert!(html.contains("A B"));
247        assert!(html.contains("C"));
248    }
249
250    #[test]
251    fn test_link_invalid_url_returns_error() {
252        let mut parser = parser_html();
253        parser = parser.add(MessageTextFormat::Link(
254            "not a url".to_string(),
255            "A".to_string(),
256        ));
257        let res = parser.parse();
258        assert!(res.is_err());
259    }
260
261    #[test]
262    fn test_empty_parser_returns_empty_string() {
263        let parser = parser_html();
264        let (html, mode) = parser.parse().unwrap();
265        assert_eq!(html, "");
266        assert_eq!(mode, ParseMode::HTML);
267    }
268
269    #[test]
270    fn test_markdownv2_parse_mode_returns_error() {
271        let parser = MessageTextParser {
272            text: vec![MessageTextFormat::Plain("Hello".to_string())],
273            parse_mode: ParseMode::MarkdownV2,
274            ..Default::default()
275        };
276        let result = parser.parse();
277        assert!(result.is_err());
278        if let Err(BotError::Validation(msg)) = result {
279            assert!(msg.contains("Parse mode not supported"));
280            assert!(msg.contains("MarkdownV2"));
281        } else {
282            panic!("Expected BotError::Validation");
283        }
284    }
285}