1use std::collections::HashMap;
2
3use once_cell::sync::Lazy;
4use regex::{Captures, Regex};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8static LI_START_TAG_REGEX: Lazy<Regex> =
9 Lazy::new(|| Regex::new(r"<li>").expect("invalid li_start regex"));
10static LI_END_TAG_REGEX: Lazy<Regex> =
11 Lazy::new(|| Regex::new(r"</li>").expect("invalid li_end regex"));
12static ALL_TAG_REGEX: Lazy<Regex> =
13 Lazy::new(|| Regex::new(r"<.+?>").expect("invalid all_tags regex"));
14static DOUBLE_QUOTE_REGEX: Lazy<Regex> =
15 Lazy::new(|| Regex::new(r#"""#).expect("invalid double_quote regex"));
16
17#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
18#[serde(tag = "name", content = "attributes")]
19pub enum BlockAttributes {
20 #[serde(rename = "core/paragraph")]
21 Paragraph {
22 content: String,
23 drop_cap: bool,
24 #[serde(flatten)]
25 rest: HashMap<String, serde_json::Value>,
26 },
27 #[serde(rename = "core/image")]
28 Image {
29 alt: String,
30 url: String,
31 #[serde(flatten)]
32 rest: HashMap<String, serde_json::Value>,
33 },
34 #[serde(rename = "core/heading")]
35 Heading {
36 content: String,
37 anchor: String,
38 level: i64,
39 #[serde(flatten)]
40 rest: HashMap<String, serde_json::Value>,
41 },
42 #[serde(rename = "core/list")]
43 List {
44 ordered: bool,
45 values: String,
46 #[serde(flatten)]
47 rest: HashMap<String, serde_json::Value>,
48 },
49}
50
51#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
52pub struct EmailGutenbergBlock {
53 #[serde(rename = "clientId")]
54 pub client_id: Uuid,
55 #[serde(rename = "isValid")]
56 pub is_valid: bool,
57 #[serde(flatten)]
58 pub attributes: BlockAttributes,
59 #[serde(rename = "innerBlocks")]
60 pub inner_blocks: Vec<EmailGutenbergBlock>,
61}
62
63pub fn process_content_to_plaintext(blocks: &[EmailGutenbergBlock]) -> String {
64 let contents: Vec<String> = blocks
65 .iter()
66 .map(|block| match &block.attributes {
67 BlockAttributes::Paragraph { content, .. } => {
68 let res = ALL_TAG_REGEX.replace_all(content, "").to_string();
69 format!("{}\n\n", res)
70 }
71 BlockAttributes::Image { alt, url, .. } => {
72 let result = DOUBLE_QUOTE_REGEX.replace_all(alt, "").to_string();
73 format!("\"{}\", <{}>", result, url)
74 }
75 BlockAttributes::Heading { content, .. } => format!("{}\n\n\n", content),
76 BlockAttributes::List {
77 values, ordered, ..
78 } => {
79 if *ordered {
80 let mut counter = 0;
81 let first_tags = LI_START_TAG_REGEX
82 .replace_all(values, |_caps: &Captures| {
83 counter += 1;
84 format!("{}. ", counter)
85 })
86 .to_string();
87 let snd_tags = LI_END_TAG_REGEX.replace_all(&first_tags, "\n").to_string();
88 ALL_TAG_REGEX.replace_all(&snd_tags, "").to_string()
89 } else {
90 let first_tags = LI_START_TAG_REGEX.replace_all(values, "* ").to_string();
91 let snd_tags = LI_END_TAG_REGEX.replace_all(&first_tags, "\n").to_string();
92 ALL_TAG_REGEX.replace_all(&snd_tags, "").to_string()
93 }
94 }
95 })
96 .collect();
97 contents.join("\n")
98}
99
100pub fn process_content_to_html(blocks: &[EmailGutenbergBlock]) -> String {
101 let contents: Vec<String> = blocks
102 .iter()
103 .map(|block| match &block.attributes {
104 BlockAttributes::Paragraph {
105 content,
106 drop_cap: _,
107 ..
108 } => {
109 format!("<p>{}</p>", content)
110 }
111 BlockAttributes::Image { alt, url, .. } => {
112 format!(r#"<img src="{}" alt="{}"></img>"#, url, alt)
113 }
114 BlockAttributes::Heading { content, level, .. } => {
115 format!("<h{}>{}</h{}>", level, content, level)
116 }
117 BlockAttributes::List {
118 values, ordered, ..
119 } => {
120 if *ordered {
121 format!("<ol>{}</ol>", values)
122 } else {
123 format!("<ul>{}</ul>", values)
124 }
125 }
126 })
127 .collect();
128 contents.join("")
129}
130
131#[cfg(test)]
132mod email_processor_tests {
133 use pretty_assertions::assert_eq;
134 use uuid::Uuid;
135
136 use super::*;
137
138 #[test]
139 fn it_converts_paragraph_correctly_to_plain_text() {
140 let input = vec![EmailGutenbergBlock {
141 client_id: Uuid::new_v4(),
142 is_valid: true,
143 attributes: BlockAttributes::Paragraph {
144 content: String::from("testi paragraph."),
145 drop_cap: false,
146 rest: HashMap::new(),
147 },
148 inner_blocks: vec![],
149 }];
150
151 let result = process_content_to_plaintext(&input);
152
153 assert_eq!(String::from("testi paragraph.\n\n"), result);
154 }
155
156 #[test]
157 fn it_converts_paragraph_wrapped_in_tags_correctly_to_plain_text() {
158 let input = vec![EmailGutenbergBlock {
159 client_id: Uuid::new_v4(),
160 is_valid: true,
161 attributes: BlockAttributes::Paragraph {
162 content: String::from("<strong><em>testi paragraph.</em></strong>"),
163 drop_cap: false,
164 rest: HashMap::new(),
165 },
166 inner_blocks: vec![],
167 }];
168
169 let result = process_content_to_plaintext(&input);
170
171 assert_eq!(String::from("testi paragraph.\n\n"), result);
172 }
173
174 #[test]
175 fn it_converts_heading_correctly_to_plain_text() {
176 let input = vec![EmailGutenbergBlock {
177 client_id: Uuid::new_v4(),
178 is_valid: true,
179 attributes: BlockAttributes::Heading {
180 content: String::from("Email heading"),
181 anchor: String::from("email-heading"),
182 level: 2,
183 rest: HashMap::new(),
184 },
185 inner_blocks: vec![],
186 }];
187
188 let result = process_content_to_plaintext(&input);
189
190 assert_eq!(String::from("Email heading\n\n\n"), result);
191 }
192
193 #[test]
194 fn it_converts_image_correctly_to_plain_text() {
195 let input = vec![EmailGutenbergBlock {
196 client_id: Uuid::new_v4(),
197 is_valid: true,
198 attributes: BlockAttributes::Image {
199 alt: String::from("Alternative title"),
200 url: String::from("URL -of an image"),
201 rest: HashMap::new(),
202 },
203 inner_blocks: vec![],
204 }];
205
206 let result = process_content_to_plaintext(&input);
207
208 assert_eq!(
209 String::from("\"Alternative title\", <URL -of an image>"),
210 result
211 );
212 }
213 #[test]
214 fn it_converts_image_containing_double_quotes_correctly_to_plain_text() {
215 let input = vec![EmailGutenbergBlock {
216 client_id: Uuid::new_v4(),
217 is_valid: true,
218 attributes: BlockAttributes::Image {
219 alt: String::from(r#""Alternative title""#),
220 url: String::from("URL -of an image"),
221 rest: HashMap::new(),
222 },
223 inner_blocks: vec![],
224 }];
225
226 let result = process_content_to_plaintext(&input);
227
228 assert_eq!(
229 String::from("\"Alternative title\", <URL -of an image>"),
230 result
231 );
232 }
233
234 #[test]
235 fn it_converts_unordered_list_correctly_to_plain_text() {
236 let input = vec![EmailGutenbergBlock {
237 client_id: Uuid::new_v4(),
238 is_valid: true,
239 attributes: BlockAttributes::List {
240 values: String::from("<li>1</li><li>2</li><li>3</li><li>4</li>"),
241 ordered: false,
242 rest: HashMap::new(),
243 },
244 inner_blocks: vec![],
245 }];
246
247 let result = process_content_to_plaintext(&input);
248
249 assert_eq!(String::from("* 1\n* 2\n* 3\n* 4\n"), result);
250 }
251
252 #[test]
253 fn it_converts_unordered_list_containing_other_tags_correctly_to_plain_text() {
254 let input = vec![EmailGutenbergBlock {
255 client_id: Uuid::new_v4(),
256 is_valid: true,
257 attributes: BlockAttributes::List {
258 values: String::from(
259 "<li><code>1</code></li><li><kbd>2</kbd></li><li>3</li><li>4</li>",
260 ),
261 ordered: false,
262 rest: HashMap::new(),
263 },
264 inner_blocks: vec![],
265 }];
266
267 let result = process_content_to_plaintext(&input);
268
269 assert_eq!(String::from("* 1\n* 2\n* 3\n* 4\n"), result);
270 }
271
272 #[test]
273 fn it_converts_ordered_list_correctly_to_plain_text() {
274 let input = vec![EmailGutenbergBlock {
275 client_id: Uuid::new_v4(),
276 is_valid: true,
277 attributes: BlockAttributes::List {
278 values: String::from("<li>first</li><li>second</li><li>third</li><li>fourth</li>"),
279 ordered: true,
280 rest: HashMap::new(),
281 },
282 inner_blocks: vec![],
283 }];
284
285 let result = process_content_to_plaintext(&input);
286
287 assert_eq!(
288 String::from("1. first\n2. second\n3. third\n4. fourth\n"),
289 result
290 );
291 }
292
293 #[test]
294 fn it_converts_ordered_list_containing_other_tags_correctly_to_plain_text() {
295 let input = vec![EmailGutenbergBlock {
296 client_id: Uuid::new_v4(),
297 is_valid: true,
298 attributes: BlockAttributes::List {
299 values: String::from(
300 "<li><code>first</code></li><li><kbd>second</kbd></li><li>third</li><li>fourth</li>",
301 ),
302 ordered: true,
303 rest: HashMap::new(),
304 },
305 inner_blocks: vec![],
306 }];
307
308 let result = process_content_to_plaintext(&input);
309
310 assert_eq!(
311 String::from("1. first\n2. second\n3. third\n4. fourth\n"),
312 result
313 );
314 }
315
316 #[test]
317 fn it_converts_paragraph_correctly_to_html() {
318 let input = vec![EmailGutenbergBlock {
319 client_id: Uuid::new_v4(),
320 is_valid: true,
321 attributes: BlockAttributes::Paragraph {
322 content: String::from("testi paragraph."),
323 drop_cap: false,
324 rest: HashMap::new(),
325 },
326 inner_blocks: vec![],
327 }];
328
329 let result = process_content_to_html(&input);
330
331 assert_eq!(String::from("<p>testi paragraph.</p>"), result);
332 }
333
334 #[test]
335 fn it_converts_heading_correctly_to_html() {
336 let input = vec![EmailGutenbergBlock {
337 client_id: Uuid::new_v4(),
338 is_valid: true,
339 attributes: BlockAttributes::Heading {
340 content: String::from("Email heading"),
341 anchor: String::from("email-heading"),
342 level: 2,
343 rest: HashMap::new(),
344 },
345 inner_blocks: vec![],
346 }];
347
348 let result = process_content_to_html(&input);
349
350 assert_eq!(String::from("<h2>Email heading</h2>"), result);
351 }
352
353 #[test]
354 fn it_converts_image_correctly_to_html() {
355 let input = vec![EmailGutenbergBlock {
356 client_id: Uuid::new_v4(),
357 is_valid: true,
358 attributes: BlockAttributes::Image {
359 alt: String::from("Alternative title"),
360 url: String::from("URL -of an image"),
361 rest: HashMap::new(),
362 },
363 inner_blocks: vec![],
364 }];
365
366 let result = process_content_to_html(&input);
367
368 assert_eq!(
369 String::from(r#"<img src="URL -of an image" alt="Alternative title"></img>"#),
370 result
371 );
372 }
373
374 #[test]
375 fn it_converts_unordered_list_correctly_to_html() {
376 let input = vec![EmailGutenbergBlock {
377 client_id: Uuid::new_v4(),
378 is_valid: true,
379 attributes: BlockAttributes::List {
380 values: String::from("<li>1</li><li>2</li><li>3</li><li>4</li>"),
381 ordered: false,
382 rest: HashMap::new(),
383 },
384 inner_blocks: vec![],
385 }];
386
387 let result = process_content_to_html(&input);
388
389 assert_eq!(
390 String::from("<ul><li>1</li><li>2</li><li>3</li><li>4</li></ul>"),
391 result
392 );
393 }
394
395 #[test]
396 fn it_converts_unordered_list_containing_other_tags_correctly_to_html() {
397 let input = vec![EmailGutenbergBlock {
398 client_id: Uuid::new_v4(),
399 is_valid: true,
400 attributes: BlockAttributes::List {
401 values: String::from(
402 "<li><code>1</code></li><li><kbd>2</kbd></li><li>3</li><li>4</li>",
403 ),
404 ordered: false,
405 rest: HashMap::new(),
406 },
407 inner_blocks: vec![],
408 }];
409
410 let result = process_content_to_html(&input);
411
412 assert_eq!(
413 String::from(
414 "<ul><li><code>1</code></li><li><kbd>2</kbd></li><li>3</li><li>4</li></ul>"
415 ),
416 result
417 );
418 }
419
420 #[test]
421 fn it_converts_ordered_list_correctly_to_html() {
422 let input = vec![EmailGutenbergBlock {
423 client_id: Uuid::new_v4(),
424 is_valid: true,
425 attributes: BlockAttributes::List {
426 values: String::from("<li>first</li><li>second</li><li>third</li><li>fourth</li>"),
427 ordered: true,
428 rest: HashMap::new(),
429 },
430 inner_blocks: vec![],
431 }];
432
433 let result = process_content_to_html(&input);
434
435 assert_eq!(
436 String::from("<ol><li>first</li><li>second</li><li>third</li><li>fourth</li></ol>"),
437 result
438 );
439 }
440
441 #[test]
442 fn it_converts_ordered_list_containing_other_tags_correctly_to_html() {
443 let input = vec![EmailGutenbergBlock {
444 client_id: Uuid::new_v4(),
445 is_valid: true,
446 attributes: BlockAttributes::List {
447 values: String::from(
448 "<li><code>first</code></li><li><kbd>second</kbd></li><li>third</li><li>fourth</li>",
449 ),
450 ordered: true,
451 rest: HashMap::new(),
452 },
453 inner_blocks: vec![],
454 }];
455
456 let result = process_content_to_html(&input);
457
458 assert_eq!(
459 String::from(
460 "<ol><li><code>first</code></li><li><kbd>second</kbd></li><li>third</li><li>fourth</li></ol>"
461 ),
462 result
463 );
464 }
465}