Skip to main content

slack_blocks_render/
text.rs

1use slack_morphism::prelude::*;
2
3use crate::{
4    references::SlackReferences,
5    visitor::{
6        visit_slack_block_mark_down_text, visit_slack_block_plain_text, visit_slack_context_block,
7        visit_slack_divider_block, visit_slack_header_block, visit_slack_markdown_block,
8        visit_slack_section_block, visit_slack_video_block, SlackRichTextBlock, Visitor,
9    },
10};
11
12/// TODO: document this function
13///
14pub fn render_blocks_as_text(blocks: Vec<SlackBlock>, slack_references: SlackReferences) -> String {
15    let mut block_renderer = TextRenderer::new(slack_references);
16    for block in blocks {
17        block_renderer.visit_slack_block(&block);
18    }
19    block_renderer.sub_texts.join("")
20}
21
22struct TextRenderer {
23    pub sub_texts: Vec<String>,
24    pub slack_references: SlackReferences,
25}
26
27impl TextRenderer {
28    pub fn new(slack_references: SlackReferences) -> Self {
29        TextRenderer {
30            sub_texts: vec![],
31            slack_references,
32        }
33    }
34}
35
36impl Visitor for TextRenderer {
37    fn visit_slack_section_block(&mut self, slack_section_block: &SlackSectionBlock) {
38        let mut section_renderer = TextRenderer::new(self.slack_references.clone());
39        visit_slack_section_block(&mut section_renderer, slack_section_block);
40        self.sub_texts.push(section_renderer.sub_texts.join(""));
41    }
42
43    fn visit_slack_block_plain_text(&mut self, slack_block_plain_text: &SlackBlockPlainText) {
44        self.sub_texts.push(slack_block_plain_text.text.clone());
45        visit_slack_block_plain_text(self, slack_block_plain_text);
46    }
47
48    fn visit_slack_header_block(&mut self, slack_header_block: &SlackHeaderBlock) {
49        let mut header_renderer = TextRenderer::new(self.slack_references.clone());
50        visit_slack_header_block(&mut header_renderer, slack_header_block);
51        self.sub_texts.push(header_renderer.sub_texts.join(""));
52    }
53
54    fn visit_slack_divider_block(&mut self, slack_divider_block: &SlackDividerBlock) {
55        self.sub_texts.push("---\n".to_string());
56        visit_slack_divider_block(self, slack_divider_block);
57    }
58
59    fn visit_slack_block_mark_down_text(
60        &mut self,
61        slack_block_mark_down_text: &SlackBlockMarkDownText,
62    ) {
63        self.sub_texts.push(slack_block_mark_down_text.text.clone());
64        visit_slack_block_mark_down_text(self, slack_block_mark_down_text);
65    }
66
67    fn visit_slack_context_block(&mut self, slack_context_block: &SlackContextBlock) {
68        let mut section_renderer = TextRenderer::new(self.slack_references.clone());
69        visit_slack_context_block(&mut section_renderer, slack_context_block);
70        self.sub_texts.push(section_renderer.sub_texts.join(""));
71    }
72
73    fn visit_slack_rich_text_block(&mut self, slack_rich_text_block: &SlackRichTextBlock) {
74        self.sub_texts.push(render_rich_text_block_as_text(
75            slack_rich_text_block.json_value.clone(),
76            &self.slack_references,
77        ));
78    }
79
80    fn visit_slack_video_block(&mut self, slack_video_block: &SlackVideoBlock) {
81        let title: SlackBlockText = slack_video_block.title.clone().into();
82        let title = match title {
83            SlackBlockText::Plain(plain_text) => plain_text.text,
84            SlackBlockText::MarkDown(md_text) => md_text.text,
85        };
86        self.sub_texts.push(title);
87
88        if let Some(description) = slack_video_block.description.clone() {
89            let description: SlackBlockText = description.into();
90            let description = match description {
91                SlackBlockText::Plain(plain_text) => plain_text.text,
92                SlackBlockText::MarkDown(md_text) => md_text.text,
93            };
94            self.sub_texts.push(format!("\n{}", description));
95        }
96
97        visit_slack_video_block(self, slack_video_block);
98    }
99
100    fn visit_slack_markdown_block(&mut self, slack_markdown_block: &SlackMarkdownBlock) {
101        self.sub_texts.push(slack_markdown_block.text.clone());
102        visit_slack_markdown_block(self, slack_markdown_block);
103    }
104}
105
106fn render_rich_text_block_as_text(
107    json_value: serde_json::Value,
108    slack_references: &SlackReferences,
109) -> String {
110    match json_value.get("elements") {
111        Some(serde_json::Value::Array(elements)) => elements
112            .iter()
113            .map(|element| {
114                match (
115                    element.get("type").map(|t| t.as_str()),
116                    element.get("style"),
117                    element.get("elements"),
118                ) {
119                    (
120                        Some(Some("rich_text_section")),
121                        None,
122                        Some(serde_json::Value::Array(elements)),
123                    ) => render_rich_text_section_elements(elements, slack_references),
124                    (
125                        Some(Some("rich_text_list")),
126                        Some(serde_json::Value::String(style)),
127                        Some(serde_json::Value::Array(elements)),
128                    ) => render_rich_text_list_elements(elements, style, slack_references),
129                    (
130                        Some(Some("rich_text_preformatted")),
131                        None,
132                        Some(serde_json::Value::Array(elements)),
133                    ) => render_rich_text_preformatted_elements(elements, slack_references),
134                    (
135                        Some(Some("rich_text_quote")),
136                        None,
137                        Some(serde_json::Value::Array(elements)),
138                    ) => render_rich_text_quote_elements(elements, slack_references),
139                    _ => "".to_string(),
140                }
141            })
142            .collect::<Vec<String>>()
143            .join(""),
144        _ => "".to_string(),
145    }
146}
147
148fn render_rich_text_section_elements(
149    elements: &[serde_json::Value],
150    slack_references: &SlackReferences,
151) -> String {
152    elements
153        .iter()
154        .map(|e| render_rich_text_section_element(e, slack_references))
155        .collect::<Vec<String>>()
156        .join("")
157}
158
159fn render_rich_text_list_elements(
160    elements: &[serde_json::Value],
161    style: &str,
162    slack_references: &SlackReferences,
163) -> String {
164    let list_style = if style == "ordered" { "1." } else { "-" };
165    elements
166        .iter()
167        .filter_map(|element| {
168            if let Some(serde_json::Value::Array(elements)) = element.get("elements") {
169                Some(render_rich_text_section_elements(
170                    elements,
171                    slack_references,
172                ))
173            } else {
174                None
175            }
176        })
177        .map(|element| format!("{list_style} {element}"))
178        .collect::<Vec<String>>()
179        .join("\n")
180}
181
182fn render_rich_text_preformatted_elements(
183    elements: &[serde_json::Value],
184    slack_references: &SlackReferences,
185) -> String {
186    render_rich_text_section_elements(elements, slack_references)
187}
188
189fn render_rich_text_quote_elements(
190    elements: &[serde_json::Value],
191    slack_references: &SlackReferences,
192) -> String {
193    render_rich_text_section_elements(elements, slack_references)
194}
195
196fn render_rich_text_section_element(
197    element: &serde_json::Value,
198    slack_references: &SlackReferences,
199) -> String {
200    match element.get("type").map(|t| t.as_str()) {
201        Some(Some("text")) => {
202            let Some(serde_json::Value::String(text)) = element.get("text") else {
203                return "".to_string();
204            };
205            text.to_string()
206        }
207        Some(Some("channel")) => {
208            let Some(serde_json::Value::String(channel_id)) = element.get("channel_id") else {
209                return "".to_string();
210            };
211            let channel_rendered = if let Some(Some(channel_name)) = slack_references
212                .channels
213                .get(&SlackChannelId(channel_id.clone()))
214            {
215                channel_name
216            } else {
217                channel_id
218            };
219            format!("#{channel_rendered}")
220        }
221        Some(Some("user")) => {
222            let Some(serde_json::Value::String(user_id)) = element.get("user_id") else {
223                return "".to_string();
224            };
225            let user_rendered = if let Some(Some(user_name)) =
226                slack_references.users.get(&SlackUserId(user_id.clone()))
227            {
228                user_name
229            } else {
230                user_id
231            };
232            format!("@{user_rendered}")
233        }
234        Some(Some("usergroup")) => {
235            let Some(serde_json::Value::String(usergroup_id)) = element.get("usergroup_id") else {
236                return "".to_string();
237            };
238            let usergroup_rendered = if let Some(Some(usergroup_name)) = slack_references
239                .usergroups
240                .get(&SlackUserGroupId(usergroup_id.clone()))
241            {
242                usergroup_name
243            } else {
244                usergroup_id
245            };
246            format!("@{usergroup_rendered}")
247        }
248        Some(Some("emoji")) => {
249            let Some(serde_json::Value::String(name)) = element.get("name") else {
250                return "".to_string();
251            };
252            let splitted = name.split("::skin-tone-").collect::<Vec<&str>>();
253            let Some(first) = splitted.first() else {
254                return "".to_string();
255            };
256            let Some(emoji) = emojis::get_by_shortcode(first) else {
257                return "".to_string();
258            };
259            let Some(skin_tone) = splitted.get(1).and_then(|s| s.parse::<usize>().ok()) else {
260                return emoji.to_string();
261            };
262            let Some(mut skin_tones) = emoji.skin_tones() else {
263                return emoji.to_string();
264            };
265            let Some(skinned_emoji) = skin_tones.nth(skin_tone - 1) else {
266                return emoji.to_string();
267            };
268            skinned_emoji.to_string()
269        }
270        // `attachment_mention` (a mention of a file or a third-party app entity, ie. a Google
271        // Drive document) and `message_mention` (a mention of another Slack message) both carry
272        // a `url` and an optional display `text`, and render like links.
273        Some(Some("link")) | Some(Some("attachment_mention")) | Some(Some("message_mention")) => {
274            let Some(serde_json::Value::String(text)) = element.get("text") else {
275                return "".to_string();
276            };
277            text.to_string()
278        }
279        _ => "".to_string(),
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use std::collections::HashMap;
286
287    use url::Url;
288
289    use super::*;
290    use crate::test_utils::rich_text_block;
291
292    #[test]
293    fn test_empty_input() {
294        assert_eq!(
295            render_blocks_as_text(vec![], SlackReferences::default()),
296            "".to_string()
297        );
298    }
299
300    #[test]
301    fn test_with_image() {
302        let blocks = vec![
303            SlackBlock::Image(SlackImageBlock::new(
304                SlackImageUrlOrFile::ImageUrl {
305                    image_url: Url::parse("https://example.com/image.png").unwrap(),
306                },
307                "Image".to_string(),
308            )),
309            SlackBlock::Image(SlackImageBlock::new(
310                SlackImageUrlOrFile::ImageUrl {
311                    image_url: Url::parse("https://example.com/image2.png").unwrap(),
312                },
313                "Image2".to_string(),
314            )),
315        ];
316        assert_eq!(
317            render_blocks_as_text(blocks, SlackReferences::default()),
318            "".to_string()
319        );
320    }
321
322    #[test]
323    fn test_with_divider() {
324        let blocks = vec![
325            SlackBlock::Divider(SlackDividerBlock::new()),
326            SlackBlock::Divider(SlackDividerBlock::new()),
327        ];
328        assert_eq!(
329            render_blocks_as_text(blocks, SlackReferences::default()),
330            "---\n---\n".to_string()
331        );
332    }
333
334    #[test]
335    fn test_with_input() {
336        // No rendering
337        let blocks = vec![SlackBlock::Input(SlackInputBlock::new(
338            "label".into(),
339            SlackInputBlockElement::PlainTextInput(
340                SlackBlockPlainTextInputElement::new().with_action_id("id".into()),
341            ),
342        ))];
343        assert_eq!(
344            render_blocks_as_text(blocks, SlackReferences::default()),
345            "".to_string()
346        );
347    }
348
349    #[test]
350    fn test_with_action() {
351        // No rendering
352        let blocks = vec![SlackBlock::Actions(SlackActionsBlock::new(vec![]))];
353        assert_eq!(
354            render_blocks_as_text(blocks, SlackReferences::default()),
355            "".to_string()
356        );
357    }
358
359    #[test]
360    fn test_with_file() {
361        // No rendering
362        let blocks = vec![SlackBlock::File(SlackFileBlock::new("external_id".into()))];
363        assert_eq!(
364            render_blocks_as_text(blocks, SlackReferences::default()),
365            "".to_string()
366        );
367    }
368
369    #[test]
370    fn test_with_video() {
371        let blocks = vec![SlackBlock::Video(
372            SlackVideoBlock::new(
373                "alt text".into(),
374                "Video title".into(),
375                "https://example.com/thumbnail.jpg".parse().unwrap(),
376                "https://example.com/video_embed.avi".parse().unwrap(),
377            )
378            .with_description("Video description".into())
379            .with_title_url("https://example.com/video".parse().unwrap()),
380        )];
381        assert_eq!(
382            render_blocks_as_text(blocks, SlackReferences::default()),
383            r#"Video title
384Video description"#
385                .to_string()
386        );
387    }
388
389    #[test]
390    fn test_with_video_minimal() {
391        let blocks = vec![SlackBlock::Video(SlackVideoBlock::new(
392            "alt text".into(),
393            "Video title".into(),
394            "https://example.com/thumbnail.jpg".parse().unwrap(),
395            "https://example.com/video_embed.avi".parse().unwrap(),
396        ))];
397        assert_eq!(
398            render_blocks_as_text(blocks, SlackReferences::default()),
399            "Video title".to_string()
400        );
401    }
402
403    #[test]
404    fn test_with_event() {
405        // No rendering
406        let blocks = vec![SlackBlock::Event(serde_json::json!({}))];
407        assert_eq!(
408            render_blocks_as_text(blocks, SlackReferences::default()),
409            "".to_string()
410        );
411    }
412
413    #[test]
414    fn test_with_unrendered_blocks() {
415        // Plan and DataTable blocks, and block types slack-morphism does not model
416        let blocks: Vec<SlackBlock> = serde_json::from_value(serde_json::json!([
417            { "type": "plan", "title": "Plan", "tasks": [] },
418            { "type": "data_table", "caption": "Caption", "rows": [] },
419            { "type": "some_future_block", "text": "Text" }
420        ]))
421        .unwrap();
422        assert!(matches!(blocks[0], SlackBlock::Plan(_)));
423        assert!(matches!(blocks[2], SlackBlock::Unknown(_)));
424        assert_eq!(
425            render_blocks_as_text(blocks, SlackReferences::default()),
426            "".to_string()
427        );
428    }
429
430    #[test]
431    fn test_with_container() {
432        let blocks = vec![SlackBlock::Container(SlackContainerBlock::new(vec![
433            SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::Plain(
434                SlackBlockPlainText::new("Text".to_string()),
435            ))),
436        ]))];
437        assert_eq!(
438            render_blocks_as_text(blocks, SlackReferences::default()),
439            "Text".to_string()
440        );
441    }
442
443    #[test]
444    fn test_header() {
445        let blocks = vec![SlackBlock::Header(SlackHeaderBlock::new("Text".into()))];
446        assert_eq!(
447            render_blocks_as_text(blocks, SlackReferences::default()),
448            "Text".to_string()
449        );
450    }
451
452    mod section {
453        use super::*;
454
455        #[test]
456        fn test_with_plain_text() {
457            let blocks = vec![
458                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::Plain(
459                    SlackBlockPlainText::new("Text".to_string()),
460                ))),
461                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::Plain(
462                    SlackBlockPlainText::new("Text2".to_string()),
463                ))),
464            ];
465            assert_eq!(
466                render_blocks_as_text(blocks, SlackReferences::default()),
467                "TextText2".to_string()
468            );
469        }
470
471        #[test]
472        fn test_with_markdown() {
473            let blocks = vec![
474                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::MarkDown(
475                    SlackBlockMarkDownText::new("Text".to_string()),
476                ))),
477                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::MarkDown(
478                    SlackBlockMarkDownText::new("Text2".to_string()),
479                ))),
480            ];
481            assert_eq!(
482                render_blocks_as_text(blocks, SlackReferences::default()),
483                "TextText2".to_string()
484            );
485        }
486
487        #[test]
488        fn test_with_fields() {
489            let blocks = vec![
490                SlackBlock::Section(SlackSectionBlock::new().with_fields(vec![
491                    SlackBlockText::Plain(SlackBlockPlainText::new("Text11".to_string())),
492                    SlackBlockText::Plain(SlackBlockPlainText::new("Text12".to_string())),
493                ])),
494                SlackBlock::Section(SlackSectionBlock::new().with_fields(vec![
495                    SlackBlockText::Plain(SlackBlockPlainText::new("Text21".to_string())),
496                    SlackBlockText::Plain(SlackBlockPlainText::new("Text22".to_string())),
497                ])),
498            ];
499            assert_eq!(
500                render_blocks_as_text(blocks, SlackReferences::default()),
501                "Text11Text12Text21Text22".to_string()
502            );
503        }
504
505        #[test]
506        fn test_with_fields_and_text() {
507            let blocks = vec![
508                SlackBlock::Section(
509                    SlackSectionBlock::new()
510                        .with_text(SlackBlockText::MarkDown(SlackBlockMarkDownText::new(
511                            "Text1".to_string(),
512                        )))
513                        .with_fields(vec![
514                            SlackBlockText::Plain(SlackBlockPlainText::new("Text11".to_string())),
515                            SlackBlockText::Plain(SlackBlockPlainText::new("Text12".to_string())),
516                        ]),
517                ),
518                SlackBlock::Section(
519                    SlackSectionBlock::new()
520                        .with_text(SlackBlockText::MarkDown(SlackBlockMarkDownText::new(
521                            "Text2".to_string(),
522                        )))
523                        .with_fields(vec![
524                            SlackBlockText::Plain(SlackBlockPlainText::new("Text21".to_string())),
525                            SlackBlockText::Plain(SlackBlockPlainText::new("Text22".to_string())),
526                        ]),
527                ),
528            ];
529            assert_eq!(
530                render_blocks_as_text(blocks, SlackReferences::default()),
531                "Text1Text11Text12Text2Text21Text22".to_string()
532            );
533        }
534    }
535
536    mod context {
537        use super::*;
538
539        #[test]
540        fn test_with_image() {
541            let blocks = vec![SlackBlock::Context(SlackContextBlock::new(vec![
542                SlackContextBlockElement::Image(SlackBlockImageElement::new(
543                    SlackImageUrlOrFile::ImageUrl {
544                        image_url: Url::parse("https://example.com/image.png").unwrap(),
545                    },
546                    "Image".to_string(),
547                )),
548                SlackContextBlockElement::Image(SlackBlockImageElement::new(
549                    SlackImageUrlOrFile::ImageUrl {
550                        image_url: Url::parse("https://example.com/image2.png").unwrap(),
551                    },
552                    "Image2".to_string(),
553                )),
554            ]))];
555            assert_eq!(
556                render_blocks_as_text(blocks, SlackReferences::default()),
557                "".to_string()
558            );
559        }
560
561        #[test]
562        fn test_with_plain_text() {
563            let blocks = vec![SlackBlock::Context(SlackContextBlock::new(vec![
564                SlackContextBlockElement::Plain(SlackBlockPlainText::new("Text".to_string())),
565                SlackContextBlockElement::Plain(SlackBlockPlainText::new("Text2".to_string())),
566            ]))];
567            assert_eq!(
568                render_blocks_as_text(blocks, SlackReferences::default()),
569                "TextText2".to_string()
570            );
571        }
572
573        #[test]
574        fn test_with_markdown() {
575            let blocks = vec![SlackBlock::Context(SlackContextBlock::new(vec![
576                SlackContextBlockElement::MarkDown(SlackBlockMarkDownText::new("Text".to_string())),
577                SlackContextBlockElement::MarkDown(SlackBlockMarkDownText::new(
578                    "Text2".to_string(),
579                )),
580            ]))];
581            assert_eq!(
582                render_blocks_as_text(blocks, SlackReferences::default()),
583                "TextText2".to_string()
584            );
585        }
586    }
587
588    mod rich_text {
589        use super::*;
590
591        #[test]
592        fn test_with_empty_json() {
593            let blocks = vec![
594                rich_text_block(serde_json::json!({})),
595                rich_text_block(serde_json::json!({})),
596            ];
597            assert_eq!(
598                render_blocks_as_text(blocks, SlackReferences::default()),
599                "".to_string()
600            );
601        }
602
603        mod rich_text_section {
604            use super::*;
605
606            mod text_element {
607                use super::*;
608
609                #[test]
610                fn test_with_text() {
611                    let blocks = vec![
612                        rich_text_block(serde_json::json!({
613                            "type": "rich_text",
614                            "elements": [
615                                {
616                                    "type": "rich_text_section",
617                                    "elements": [
618                                        {
619                                            "type": "text",
620                                            "text": "Text111"
621                                        },
622                                        {
623                                            "type": "text",
624                                            "text": "Text112"
625                                        }
626                                    ]
627                                },
628                                {
629                                    "type": "rich_text_section",
630                                    "elements": [
631                                        {
632                                            "type": "text",
633                                            "text": "Text121"
634                                        },
635                                        {
636                                            "type": "text",
637                                            "text": "Text122"
638                                        }
639                                    ]
640                                }
641                            ]
642                        })),
643                        rich_text_block(serde_json::json!({
644                            "type": "rich_text",
645                            "elements": [
646                                {
647                                    "type": "rich_text_section",
648                                    "elements": [
649                                        {
650                                            "type": "text",
651                                            "text": "Text211"
652                                        },
653                                        {
654                                            "type": "text",
655                                            "text": "Text212"
656                                        }
657                                    ]
658                                },
659                                {
660                                    "type": "rich_text_section",
661                                    "elements": [
662                                        {
663                                            "type": "text",
664                                            "text": "Text221"
665                                        },
666                                        {
667                                            "type": "text",
668                                            "text": "Text222"
669                                        }
670                                    ]
671                                }
672                            ]
673                        })),
674                    ];
675                    assert_eq!(
676                        render_blocks_as_text(blocks, SlackReferences::default()),
677                        "Text111Text112Text121Text122Text211Text212Text221Text222".to_string()
678                    );
679                }
680
681                #[test]
682                fn test_with_bold_text() {
683                    let blocks = vec![rich_text_block(serde_json::json!({
684                        "type": "rich_text",
685                        "elements": [
686                            {
687                                "type": "rich_text_section",
688                                "elements": [
689                                    {
690                                        "type": "text",
691                                        "text": "Text",
692                                        "style": {
693                                            "bold": true
694                                        }
695                                    }
696                                ]
697                            }
698                        ]
699                    }))];
700                    assert_eq!(
701                        render_blocks_as_text(blocks, SlackReferences::default()),
702                        "Text".to_string()
703                    );
704                }
705
706                #[test]
707                fn test_with_italic_text() {
708                    let blocks = vec![rich_text_block(serde_json::json!({
709                        "type": "rich_text",
710                        "elements": [
711                            {
712                                "type": "rich_text_section",
713                                "elements": [
714                                    {
715                                        "type": "text",
716                                        "text": "Text",
717                                        "style": {
718                                            "italic": true
719                                        }
720                                    }
721                                ]
722                            }
723                        ]
724                    }))];
725                    assert_eq!(
726                        render_blocks_as_text(blocks, SlackReferences::default()),
727                        "Text".to_string()
728                    );
729                }
730
731                #[test]
732                fn test_with_strike_text() {
733                    let blocks = vec![rich_text_block(serde_json::json!({
734                        "type": "rich_text",
735                        "elements": [
736                            {
737                                "type": "rich_text_section",
738                                "elements": [
739                                    {
740                                        "type": "text",
741                                        "text": "Text",
742                                        "style": {
743                                            "strike": true
744                                        }
745                                    }
746                                ]
747                            }
748                        ]
749                    }))];
750                    assert_eq!(
751                        render_blocks_as_text(blocks, SlackReferences::default()),
752                        "Text".to_string()
753                    );
754                }
755
756                #[test]
757                fn test_with_code_text() {
758                    let blocks = vec![rich_text_block(serde_json::json!({
759                        "type": "rich_text",
760                        "elements": [
761                            {
762                                "type": "rich_text_section",
763                                "elements": [
764                                    {
765                                        "type": "text",
766                                        "text": "Text",
767                                        "style": {
768                                            "code": true
769                                        }
770                                    }
771                                ]
772                            }
773                        ]
774                    }))];
775                    assert_eq!(
776                        render_blocks_as_text(blocks, SlackReferences::default()),
777                        "Text".to_string()
778                    );
779                }
780
781                #[test]
782                fn test_with_styled_text() {
783                    let blocks = vec![rich_text_block(serde_json::json!({
784                        "type": "rich_text",
785                        "elements": [
786                            {
787                                "type": "rich_text_section",
788                                "elements": [
789                                    {
790                                        "type": "text",
791                                        "text": "Text",
792                                        "style": {
793                                            "bold": true,
794                                            "italic": true,
795                                            "strike": true
796                                        }
797                                    }
798                                ]
799                            }
800                        ]
801                    }))];
802                    assert_eq!(
803                        render_blocks_as_text(blocks, SlackReferences::default()),
804                        "Text".to_string()
805                    );
806                }
807            }
808
809            mod channel_element {
810                use super::*;
811
812                #[test]
813                fn test_with_channel_id() {
814                    let blocks = vec![rich_text_block(serde_json::json!({
815                        "type": "rich_text",
816                        "elements": [
817                            {
818                                "type": "rich_text_section",
819                                "elements": [
820                                    {
821                                        "type": "channel",
822                                        "channel_id": "C0123456"
823                                    }
824                                ]
825                            }
826                        ]
827                    }))];
828                    assert_eq!(
829                        render_blocks_as_text(blocks, SlackReferences::default()),
830                        "#C0123456".to_string()
831                    );
832                }
833
834                #[test]
835                fn test_with_channel_id_and_reference() {
836                    let blocks = vec![rich_text_block(serde_json::json!({
837                        "type": "rich_text",
838                        "elements": [
839                            {
840                                "type": "rich_text_section",
841                                "elements": [
842                                    {
843                                        "type": "channel",
844                                        "channel_id": "C0123456"
845                                    }
846                                ]
847                            }
848                        ]
849                    }))];
850                    assert_eq!(
851                        render_blocks_as_text(
852                            blocks,
853                            SlackReferences {
854                                channels: HashMap::from([(
855                                    SlackChannelId("C0123456".to_string()),
856                                    Some("general".to_string())
857                                )]),
858                                ..SlackReferences::default()
859                            }
860                        ),
861                        "#general".to_string()
862                    );
863                }
864            }
865
866            mod user_element {
867                use super::*;
868
869                #[test]
870                fn test_with_user_id() {
871                    let blocks = vec![rich_text_block(serde_json::json!({
872                        "type": "rich_text",
873                        "elements": [
874                            {
875                                "type": "rich_text_section",
876                                "elements": [
877                                    {
878                                        "type": "user",
879                                        "user_id": "user1"
880                                    }
881                                ]
882                            }
883                        ]
884                    }))];
885                    assert_eq!(
886                        render_blocks_as_text(blocks, SlackReferences::default()),
887                        "@user1".to_string()
888                    );
889                }
890
891                #[test]
892                fn test_with_user_id_and_reference() {
893                    let blocks = vec![rich_text_block(serde_json::json!({
894                        "type": "rich_text",
895                        "elements": [
896                            {
897                                "type": "rich_text_section",
898                                "elements": [
899                                    {
900                                        "type": "user",
901                                        "user_id": "user1"
902                                    }
903                                ]
904                            }
905                        ]
906                    }))];
907                    assert_eq!(
908                        render_blocks_as_text(
909                            blocks,
910                            SlackReferences {
911                                users: HashMap::from([(
912                                    SlackUserId("user1".to_string()),
913                                    Some("John Doe".to_string())
914                                )]),
915                                ..SlackReferences::default()
916                            }
917                        ),
918                        "@John Doe".to_string()
919                    );
920                }
921            }
922
923            mod usergroup_element {
924                use super::*;
925
926                #[test]
927                fn test_with_usergroup_id() {
928                    let blocks = vec![rich_text_block(serde_json::json!({
929                        "type": "rich_text",
930                        "elements": [
931                            {
932                                "type": "rich_text_section",
933                                "elements": [
934                                    {
935                                        "type": "usergroup",
936                                        "usergroup_id": "group1"
937                                    }
938                                ]
939                            }
940                        ]
941                    }))];
942                    assert_eq!(
943                        render_blocks_as_text(blocks, SlackReferences::default()),
944                        "@group1".to_string()
945                    );
946                }
947
948                #[test]
949                fn test_with_usergroup_id_and_reference() {
950                    let blocks = vec![rich_text_block(serde_json::json!({
951                        "type": "rich_text",
952                        "elements": [
953                            {
954                                "type": "rich_text_section",
955                                "elements": [
956                                    {
957                                        "type": "usergroup",
958                                        "usergroup_id": "group1"
959                                    }
960                                ]
961                            }
962                        ]
963                    }))];
964                    assert_eq!(
965                        render_blocks_as_text(
966                            blocks,
967                            SlackReferences {
968                                usergroups: HashMap::from([(
969                                    SlackUserGroupId("group1".to_string()),
970                                    Some("Admins".to_string())
971                                )]),
972                                ..SlackReferences::default()
973                            }
974                        ),
975                        "@Admins".to_string()
976                    );
977                }
978            }
979
980            mod link_element {
981                use super::*;
982
983                #[test]
984                fn test_with_url() {
985                    let blocks = vec![rich_text_block(serde_json::json!({
986                        "type": "rich_text",
987                        "elements": [
988                            {
989                                "type": "rich_text_section",
990                                "elements": [
991                                    {
992                                        "type": "link",
993                                        "text": "example",
994                                        "url": "https://example.com"
995                                    }
996                                ]
997                            }
998                        ]
999                    }))];
1000                    assert_eq!(
1001                        render_blocks_as_text(blocks, SlackReferences::default()),
1002                        "example".to_string()
1003                    );
1004                }
1005            }
1006
1007            mod attachment_mention_element {
1008                use super::*;
1009
1010                #[test]
1011                fn test_with_text() {
1012                    let blocks = vec![rich_text_block(serde_json::json!({
1013                        "type": "rich_text",
1014                        "elements": [
1015                            {
1016                                "type": "rich_text_section",
1017                                "elements": [
1018                                    {
1019                                        "type": "attachment_mention",
1020                                        "url": "https://docs.google.com/document/d/1abc/edit?tab=t.0",
1021                                        "text": "Project plan",
1022                                        "product_name": "Google Docs"
1023                                    }
1024                                ]
1025                            }
1026                        ]
1027                    }))];
1028                    assert_eq!(
1029                        render_blocks_as_text(blocks, SlackReferences::default()),
1030                        "Project plan".to_string()
1031                    );
1032                }
1033            }
1034
1035            mod message_mention_element {
1036                use super::*;
1037
1038                #[test]
1039                fn test_with_text() {
1040                    let blocks = vec![rich_text_block(serde_json::json!({
1041                        "type": "rich_text",
1042                        "elements": [
1043                            {
1044                                "type": "rich_text_section",
1045                                "elements": [
1046                                    {
1047                                        "type": "message_mention",
1048                                        "url": "https://slack.com/archives/C123/p1788850799491249",
1049                                        "text": "a previous message"
1050                                    }
1051                                ]
1052                            }
1053                        ]
1054                    }))];
1055                    assert_eq!(
1056                        render_blocks_as_text(blocks, SlackReferences::default()),
1057                        "a previous message".to_string()
1058                    );
1059                }
1060            }
1061
1062            mod emoji_element {
1063                use super::*;
1064
1065                #[test]
1066                fn test_with_emoji() {
1067                    let blocks = vec![rich_text_block(serde_json::json!({
1068                        "type": "rich_text",
1069                        "elements": [
1070                            {
1071                                "type": "rich_text_section",
1072                                "elements": [
1073                                    {
1074                                        "type": "emoji",
1075                                        "name": "wave"
1076                                    }
1077                                ]
1078                            }
1079                        ]
1080                    }))];
1081                    assert_eq!(
1082                        render_blocks_as_text(blocks, SlackReferences::default()),
1083                        "👋".to_string()
1084                    );
1085                }
1086
1087                #[test]
1088                fn test_with_emoji_with_skin_tone() {
1089                    let blocks = vec![rich_text_block(serde_json::json!({
1090                        "type": "rich_text",
1091                        "elements": [
1092                            {
1093                                "type": "rich_text_section",
1094                                "elements": [
1095                                    {
1096                                        "type": "emoji",
1097                                        "name": "wave::skin-tone-2"
1098                                    }
1099                                ]
1100                            }
1101                        ]
1102                    }))];
1103                    assert_eq!(
1104                        render_blocks_as_text(blocks, SlackReferences::default()),
1105                        "👋🏻".to_string()
1106                    );
1107                }
1108
1109                #[test]
1110                fn test_with_emoji_with_unknown_skin_tone() {
1111                    let blocks = vec![rich_text_block(serde_json::json!({
1112                        "type": "rich_text",
1113                        "elements": [
1114                            {
1115                                "type": "rich_text_section",
1116                                "elements": [
1117                                    {
1118                                        "type": "emoji",
1119                                        "name": "wave::skin-tone-42"
1120                                    }
1121                                ]
1122                            }
1123                        ]
1124                    }))];
1125                    assert_eq!(
1126                        render_blocks_as_text(blocks, SlackReferences::default()),
1127                        "👋".to_string()
1128                    );
1129                }
1130
1131                #[test]
1132                fn test_with_unknown_emoji() {
1133                    let blocks = vec![rich_text_block(serde_json::json!({
1134                        "type": "rich_text",
1135                        "elements": [
1136                            {
1137                                "type": "rich_text_section",
1138                                "elements": [
1139                                    {
1140                                        "type": "emoji",
1141                                        "name": "bbb"
1142                                    }
1143                                ]
1144                            }
1145                        ]
1146                    }))];
1147                    assert_eq!(
1148                        render_blocks_as_text(blocks, SlackReferences::default()),
1149                        "".to_string()
1150                    );
1151                }
1152            }
1153        }
1154
1155        mod rich_text_list {
1156            use super::*;
1157
1158            #[test]
1159            fn test_with_ordered_list() {
1160                let blocks = vec![rich_text_block(serde_json::json!({
1161                    "type": "rich_text",
1162                    "elements": [
1163                        {
1164                            "type": "rich_text_list",
1165                            "style": "ordered",
1166                            "elements": [
1167                                {
1168                                    "type": "rich_text_section",
1169                                    "elements": [
1170                                        {
1171                                            "type": "text",
1172                                            "text": "Text1"
1173                                        }
1174                                    ]
1175                                },
1176                                {
1177                                    "type": "rich_text_section",
1178                                    "elements": [
1179                                        {
1180                                            "type": "text",
1181                                            "text": "Text2"
1182                                        }
1183                                    ]
1184                                }
1185                            ]
1186                         },
1187                    ]
1188                }))];
1189                assert_eq!(
1190                    render_blocks_as_text(blocks, SlackReferences::default()),
1191                    "1. Text1\n1. Text2".to_string()
1192                );
1193            }
1194
1195            #[test]
1196            fn test_with_bullet_list() {
1197                let blocks = vec![rich_text_block(serde_json::json!({
1198                    "type": "rich_text",
1199                    "elements": [
1200                        {
1201                            "type": "rich_text_list",
1202                            "style": "bullet",
1203                            "elements": [
1204                                {
1205                                    "type": "rich_text_section",
1206                                    "elements": [
1207                                        {
1208                                            "type": "text",
1209                                            "text": "Text1"
1210                                        }
1211                                    ]
1212                                },
1213                                {
1214                                    "type": "rich_text_section",
1215                                    "elements": [
1216                                        {
1217                                            "type": "text",
1218                                            "text": "Text2"
1219                                        }
1220                                    ]
1221                                }
1222                            ]
1223                        },
1224                    ]
1225                }))];
1226                assert_eq!(
1227                    render_blocks_as_text(blocks, SlackReferences::default()),
1228                    "- Text1\n- Text2".to_string()
1229                );
1230            }
1231        }
1232
1233        mod rich_text_preformatted {
1234            use super::*;
1235
1236            #[test]
1237            fn test_with_text() {
1238                let blocks = vec![rich_text_block(serde_json::json!({
1239                    "type": "rich_text",
1240                    "elements": [
1241                        {
1242                            "type": "rich_text_preformatted",
1243                            "elements": [
1244                                {
1245                                    "type": "text",
1246                                    "text": "Text1"
1247                                },
1248                                {
1249                                    "type": "text",
1250                                    "text": "Text2"
1251                                }
1252                            ]
1253                        },
1254                    ]
1255                }))];
1256                assert_eq!(
1257                    render_blocks_as_text(blocks, SlackReferences::default()),
1258                    "Text1Text2".to_string()
1259                );
1260            }
1261        }
1262
1263        mod rich_text_quote {
1264            use super::*;
1265
1266            #[test]
1267            fn test_with_text() {
1268                let blocks = vec![rich_text_block(serde_json::json!({
1269                    "type": "rich_text",
1270                    "elements": [
1271                        {
1272                            "type": "rich_text_quote",
1273                            "elements": [
1274                                {
1275                                    "type": "text",
1276                                    "text": "Text1"
1277                                },
1278                                {
1279                                    "type": "text",
1280                                    "text": "Text2"
1281                                }
1282                            ]
1283                        },
1284                    ]
1285                }))];
1286                assert_eq!(
1287                    render_blocks_as_text(blocks, SlackReferences::default()),
1288                    "Text1Text2".to_string()
1289                );
1290            }
1291        }
1292    }
1293}