vkteams_bot/api/utils/
parser.rs1use crate::api::types::*;
2use crate::error::{BotError, Result};
3use reqwest::Url;
4use std::convert::From;
5const HTML_LIST_ITEM_OVERHEAD: usize = 10; pub trait MessageTextHTMLParser {
7 fn new() -> Self
9 where
10 Self: Sized + Default,
11 {
12 Self::default()
13 }
14 fn add(&mut self, text: MessageTextFormat) -> Self;
16 fn next_line(&mut self) -> Self;
18 fn space(&mut self) -> Self;
20 fn parse(&self) -> Result<(String, ParseMode)>;
22}
23impl MessageTextParser {
24 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 fn replace_chars(&self, text: &str) -> String {
83 text.replace('&', "&")
84 .replace('<', "<")
85 .replace('>', ">")
86 }
87}
88impl MessageTextHTMLParser for MessageTextParser {
89 fn add(&mut self, text: MessageTextFormat) -> Self {
93 self.text.push(text);
94 self.to_owned()
95 }
96 fn next_line(&mut self) -> Self {
98 self.text.push(MessageTextFormat::Plain(String::from("\n")));
99 self.to_owned()
100 }
101 fn space(&mut self) -> Self {
103 self.text.push(MessageTextFormat::Plain(String::from(" ")));
104 self.to_owned()
105 }
106 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 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 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()); }
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, "<tag>&text>");
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}