slack_blocks_render/
markdown.rs

1use serde_json::Value;
2use slack_morphism::prelude::*;
3
4use crate::{
5    references::SlackReferences,
6    visitor::{
7        visit_slack_block_image_element, visit_slack_block_mark_down_text,
8        visit_slack_block_plain_text, visit_slack_context_block, visit_slack_divider_block,
9        visit_slack_header_block, visit_slack_image_block, visit_slack_markdown_block,
10        visit_slack_section_block, visit_slack_video_block, SlackRichTextBlock, Visitor,
11    },
12};
13
14/// TODO: document this function
15///
16pub fn render_blocks_as_markdown(
17    blocks: Vec<SlackBlock>,
18    slack_references: SlackReferences,
19    handle_delimiter: Option<String>,
20) -> String {
21    let mut block_renderer = MarkdownRenderer::new(slack_references, handle_delimiter);
22    for block in blocks {
23        block_renderer.visit_slack_block(&block);
24    }
25    block_renderer.sub_texts.join("\n")
26}
27
28struct MarkdownRenderer {
29    pub sub_texts: Vec<String>,
30    pub slack_references: SlackReferences,
31    pub handle_delimiter: Option<String>,
32}
33
34impl MarkdownRenderer {
35    pub fn new(slack_references: SlackReferences, handle_delimiter: Option<String>) -> Self {
36        MarkdownRenderer {
37            sub_texts: vec![],
38            slack_references,
39            handle_delimiter,
40        }
41    }
42}
43
44fn join(mut texts: Vec<String>, join_str: &str) -> String {
45    for i in 0..texts.len() {
46        if i < texts.len() - 1 {
47            if texts[i].ends_with('`') && texts[i + 1].starts_with('`') {
48                texts[i].pop();
49                texts[i + 1].remove(0);
50            }
51            if texts[i].ends_with('~') && texts[i + 1].starts_with('~') {
52                texts[i].pop();
53                texts[i + 1].remove(0);
54            }
55            if texts[i].ends_with('_') && texts[i + 1].starts_with('_') {
56                texts[i].pop();
57                texts[i + 1].remove(0);
58            }
59            if texts[i].ends_with('*') && texts[i + 1].starts_with('*') {
60                texts[i].pop();
61                texts[i + 1].remove(0);
62            }
63            if texts[i].starts_with("> ") && !texts[i + 1].starts_with("> ") {
64                texts[i].push('\n');
65            }
66        }
67    }
68    texts.join(join_str)
69}
70
71impl Visitor for MarkdownRenderer {
72    fn visit_slack_section_block(&mut self, slack_section_block: &SlackSectionBlock) {
73        let mut section_renderer =
74            MarkdownRenderer::new(self.slack_references.clone(), self.handle_delimiter.clone());
75        visit_slack_section_block(&mut section_renderer, slack_section_block);
76        self.sub_texts.push(join(section_renderer.sub_texts, ""));
77    }
78
79    fn visit_slack_block_plain_text(&mut self, slack_block_plain_text: &SlackBlockPlainText) {
80        self.sub_texts.push(slack_block_plain_text.text.clone());
81        visit_slack_block_plain_text(self, slack_block_plain_text);
82    }
83
84    fn visit_slack_header_block(&mut self, slack_header_block: &SlackHeaderBlock) {
85        let mut header_renderer =
86            MarkdownRenderer::new(self.slack_references.clone(), self.handle_delimiter.clone());
87        visit_slack_header_block(&mut header_renderer, slack_header_block);
88        self.sub_texts
89            .push(format!("## {}", join(header_renderer.sub_texts, "")));
90    }
91
92    fn visit_slack_divider_block(&mut self, slack_divider_block: &SlackDividerBlock) {
93        self.sub_texts.push("---\n".to_string());
94        visit_slack_divider_block(self, slack_divider_block);
95    }
96
97    fn visit_slack_image_block(&mut self, slack_image_block: &SlackImageBlock) {
98        self.sub_texts.push(format!(
99            "![{}]({})",
100            slack_image_block.alt_text, slack_image_block.image_url
101        ));
102        visit_slack_image_block(self, slack_image_block);
103    }
104
105    fn visit_slack_block_image_element(
106        &mut self,
107        slack_block_image_element: &SlackBlockImageElement,
108    ) {
109        self.sub_texts.push(format!(
110            "![{}]({})",
111            slack_block_image_element.alt_text, slack_block_image_element.image_url
112        ));
113        visit_slack_block_image_element(self, slack_block_image_element);
114    }
115
116    fn visit_slack_block_mark_down_text(
117        &mut self,
118        slack_block_mark_down_text: &SlackBlockMarkDownText,
119    ) {
120        self.sub_texts.push(slack_block_mark_down_text.text.clone());
121        visit_slack_block_mark_down_text(self, slack_block_mark_down_text);
122    }
123
124    fn visit_slack_context_block(&mut self, slack_context_block: &SlackContextBlock) {
125        let mut section_renderer =
126            MarkdownRenderer::new(self.slack_references.clone(), self.handle_delimiter.clone());
127        visit_slack_context_block(&mut section_renderer, slack_context_block);
128        self.sub_texts.push(section_renderer.sub_texts.join(""));
129    }
130
131    fn visit_slack_rich_text_block(&mut self, slack_rich_text_block: &SlackRichTextBlock) {
132        self.sub_texts.push(render_rich_text_block_as_markdown(
133            slack_rich_text_block.json_value.clone(),
134            self,
135        ));
136    }
137
138    fn visit_slack_video_block(&mut self, slack_video_block: &SlackVideoBlock) {
139        let title: SlackBlockText = slack_video_block.title.clone().into();
140        let title = match title {
141            SlackBlockText::Plain(plain_text) => plain_text.text,
142            SlackBlockText::MarkDown(md_text) => md_text.text,
143        };
144        if let Some(ref title_url) = slack_video_block.title_url {
145            self.sub_texts
146                .push(format!("*[{}]({})*\n", title, title_url));
147        } else {
148            self.sub_texts.push(format!("*{}*\n", title));
149        }
150
151        if let Some(description) = slack_video_block.description.clone() {
152            let description: SlackBlockText = description.into();
153            let description = match description {
154                SlackBlockText::Plain(plain_text) => plain_text.text,
155                SlackBlockText::MarkDown(md_text) => md_text.text,
156            };
157            self.sub_texts.push(format!("{}\n", description));
158        }
159
160        self.sub_texts.push(format!(
161            "![{}]({})",
162            slack_video_block.alt_text, slack_video_block.thumbnail_url
163        ));
164
165        visit_slack_video_block(self, slack_video_block);
166    }
167
168    fn visit_slack_markdown_block(&mut self, slack_markdown_block: &SlackMarkdownBlock) {
169        self.sub_texts.push(slack_markdown_block.text.clone());
170        visit_slack_markdown_block(self, slack_markdown_block);
171    }
172}
173
174fn render_rich_text_block_as_markdown(
175    json_value: serde_json::Value,
176    renderer: &MarkdownRenderer,
177) -> String {
178    match json_value.get("elements") {
179        Some(serde_json::Value::Array(elements)) => join(
180            elements
181                .iter()
182                .map(|element| {
183                    match (
184                        element.get("type").map(|t| t.as_str()),
185                        element.get("style"),
186                        element.get("elements"),
187                        element.get("indent"),
188                    ) {
189                        (
190                            Some(Some("rich_text_section")),
191                            _,
192                            Some(serde_json::Value::Array(elements)),
193                            _,
194                        ) => render_rich_text_section_elements(elements, renderer, true),
195                        (
196                            Some(Some("rich_text_list")),
197                            Some(serde_json::Value::String(style)),
198                            Some(serde_json::Value::Array(elements)),
199                            Some(serde_json::Value::Number(indent)),
200                        ) => render_rich_text_list_elements(
201                            elements,
202                            style,
203                            indent
204                                .as_u64()
205                                .unwrap_or_default()
206                                .try_into()
207                                .unwrap_or_default(),
208                            renderer,
209                        ),
210                        (
211                            Some(Some("rich_text_list")),
212                            Some(serde_json::Value::String(style)),
213                            Some(serde_json::Value::Array(elements)),
214                            _,
215                        ) => render_rich_text_list_elements(elements, style, 0, renderer),
216                        (
217                            Some(Some("rich_text_preformatted")),
218                            _,
219                            Some(serde_json::Value::Array(elements)),
220                            _,
221                        ) => render_rich_text_preformatted_elements(elements, renderer),
222
223                        (
224                            Some(Some("rich_text_quote")),
225                            _,
226                            Some(serde_json::Value::Array(elements)),
227                            _,
228                        ) => render_rich_text_quote_elements(elements, renderer),
229
230                        _ => "".to_string(),
231                    }
232                })
233                .collect::<Vec<String>>(),
234            "\n",
235        ),
236        _ => "".to_string(),
237    }
238}
239
240fn render_rich_text_section_elements(
241    elements: &[serde_json::Value],
242    renderer: &MarkdownRenderer,
243    fix_newlines_in_text: bool,
244) -> String {
245    let result = join(
246        elements
247            .iter()
248            .map(|e| render_rich_text_section_element(e, renderer))
249            .collect::<Vec<String>>(),
250        "",
251    );
252    if fix_newlines_in_text {
253        fix_newlines(result)
254    } else {
255        result
256    }
257}
258
259fn render_rich_text_list_elements(
260    elements: &[serde_json::Value],
261    style: &str,
262    indent: usize,
263    renderer: &MarkdownRenderer,
264) -> String {
265    let list_style = if style == "ordered" { "1." } else { "-" };
266    let space_per_level = if style == "ordered" { 3 } else { 2 };
267    let indent_prefix = " ".repeat(space_per_level * indent);
268    elements
269        .iter()
270        .filter_map(|element| {
271            if let Some(serde_json::Value::Array(elements)) = element.get("elements") {
272                Some(render_rich_text_section_elements(elements, renderer, true))
273            } else {
274                None
275            }
276        })
277        .map(|element| format!("{indent_prefix}{list_style} {element}"))
278        .collect::<Vec<String>>()
279        .join("\n")
280}
281
282fn render_rich_text_preformatted_elements(
283    elements: &[serde_json::Value],
284    renderer: &MarkdownRenderer,
285) -> String {
286    format!(
287        "```\n{}\n```",
288        render_rich_text_section_elements(elements, renderer, false)
289    )
290}
291
292fn render_rich_text_quote_elements(
293    elements: &[serde_json::Value],
294    renderer: &MarkdownRenderer,
295) -> String {
296    format!(
297        "> {}",
298        render_rich_text_section_elements(elements, renderer, true)
299    )
300}
301
302fn render_rich_text_section_element(
303    element: &serde_json::Value,
304    renderer: &MarkdownRenderer,
305) -> String {
306    let handle_delimiter = renderer.handle_delimiter.clone().unwrap_or_default();
307    match element.get("type").map(|t| t.as_str()) {
308        Some(Some("text")) => {
309            let Some(serde_json::Value::String(text)) = element.get("text") else {
310                return "".to_string();
311            };
312            let style = element.get("style");
313            apply_all_styles(text.to_string(), style)
314        }
315        Some(Some("channel")) => {
316            let Some(serde_json::Value::String(channel_id)) = element.get("channel_id") else {
317                return "".to_string();
318            };
319            let channel_rendered = if let Some(Some(channel_name)) = renderer
320                .slack_references
321                .channels
322                .get(&SlackChannelId(channel_id.clone()))
323            {
324                channel_name
325            } else {
326                channel_id
327            };
328            let style = element.get("style");
329            apply_all_styles(format!("#{channel_rendered}"), style)
330        }
331        Some(Some("user")) => {
332            let Some(serde_json::Value::String(user_id)) = element.get("user_id") else {
333                return "".to_string();
334            };
335            let user_rendered = if let Some(Some(user_name)) = renderer
336                .slack_references
337                .users
338                .get(&SlackUserId(user_id.clone()))
339            {
340                user_name
341            } else {
342                user_id
343            };
344            let style = element.get("style");
345            apply_all_styles(
346                format!("{handle_delimiter}@{user_rendered}{handle_delimiter}"),
347                style,
348            )
349        }
350        Some(Some("usergroup")) => {
351            let Some(serde_json::Value::String(usergroup_id)) = element.get("usergroup_id") else {
352                return "".to_string();
353            };
354            let usergroup_rendered = if let Some(Some(usergroup_name)) = renderer
355                .slack_references
356                .usergroups
357                .get(&SlackUserGroupId(usergroup_id.clone()))
358            {
359                usergroup_name
360            } else {
361                usergroup_id
362            };
363            let style = element.get("style");
364            apply_all_styles(
365                format!("{handle_delimiter}@{usergroup_rendered}{handle_delimiter}"),
366                style,
367            )
368        }
369        Some(Some("emoji")) => {
370            let Some(serde_json::Value::String(name)) = element.get("name") else {
371                return "".to_string();
372            };
373            let style = element.get("style");
374            render_emoji(
375                &SlackEmojiName(name.to_string()),
376                &renderer.slack_references,
377                style,
378            )
379        }
380        Some(Some("link")) => {
381            let Some(serde_json::Value::String(url)) = element.get("url") else {
382                return "".to_string();
383            };
384            let Some(serde_json::Value::String(text)) = element.get("text") else {
385                return render_url_as_markdown(url, url);
386            };
387            let style = element.get("style");
388            apply_all_styles(render_url_as_markdown(url, text), style)
389        }
390        _ => "".to_string(),
391    }
392}
393
394fn render_url_as_markdown(url: &str, text: &str) -> String {
395    format!("[{}]({})", text, url)
396}
397
398fn render_emoji(
399    emoji_name: &SlackEmojiName,
400    slack_references: &SlackReferences,
401    style: Option<&Value>,
402) -> String {
403    if let Some(Some(emoji)) = slack_references.emojis.get(emoji_name) {
404        match emoji {
405            SlackEmojiRef::Alias(alias) => {
406                return render_emoji(alias, slack_references, style);
407            }
408            SlackEmojiRef::Url(url) => {
409                return apply_all_styles(format!("![:{}:]({})", emoji_name.0, url), style);
410            }
411        }
412    }
413    let name = &emoji_name.0;
414
415    let splitted = name.split("::skin-tone-").collect::<Vec<&str>>();
416    let Some(first) = splitted.first() else {
417        return apply_all_styles(format!(":{}:", name), style);
418    };
419    let Some(emoji) = emojis::get_by_shortcode(first) else {
420        return apply_all_styles(format!(":{}:", name), style);
421    };
422    let Some(skin_tone) = splitted.get(1).and_then(|s| s.parse::<usize>().ok()) else {
423        return apply_all_styles(emoji.to_string(), style);
424    };
425    let Some(mut skin_tones) = emoji.skin_tones() else {
426        return apply_all_styles(emoji.to_string(), style);
427    };
428    let Some(skinned_emoji) = skin_tones.nth(skin_tone - 1) else {
429        return apply_all_styles(emoji.to_string(), style);
430    };
431    apply_all_styles(skinned_emoji.to_string(), style)
432}
433
434fn apply_all_styles(text: String, style: Option<&serde_json::Value>) -> String {
435    let text = apply_bold_style(text, style);
436    let text = apply_italic_style(text, style);
437    let text = apply_strike_style(text, style);
438    apply_code_style(text, style)
439}
440
441fn apply_bold_style(text: String, style: Option<&serde_json::Value>) -> String {
442    if is_styled(style, "bold") {
443        format!("*{}*", text)
444    } else {
445        text
446    }
447}
448
449fn apply_italic_style(text: String, style: Option<&serde_json::Value>) -> String {
450    if is_styled(style, "italic") {
451        format!("_{}_", text)
452    } else {
453        text
454    }
455}
456
457fn apply_strike_style(text: String, style: Option<&serde_json::Value>) -> String {
458    if is_styled(style, "strike") {
459        format!("~{}~", text)
460    } else {
461        text
462    }
463}
464
465fn apply_code_style(text: String, style: Option<&serde_json::Value>) -> String {
466    if is_styled(style, "code") {
467        format!("`{}`", text)
468    } else {
469        text
470    }
471}
472
473fn is_styled(style: Option<&serde_json::Value>, style_name: &str) -> bool {
474    style
475        .and_then(|s| s.get(style_name).map(|b| b.as_bool()))
476        .flatten()
477        .unwrap_or_default()
478}
479
480fn fix_newlines(text: String) -> String {
481    text.replace("\n", "\\\n")
482        .trim_end_matches("\\\n")
483        .to_string()
484}
485
486#[cfg(test)]
487mod tests {
488    use std::collections::HashMap;
489
490    use url::Url;
491
492    use super::*;
493
494    #[test]
495    fn test_empty_input() {
496        assert_eq!(
497            render_blocks_as_markdown(vec![], SlackReferences::default(), None),
498            "".to_string()
499        );
500    }
501
502    #[test]
503    fn test_with_image() {
504        let blocks = vec![
505            SlackBlock::Image(SlackImageBlock::new(
506                Url::parse("https://example.com/image.png").unwrap(),
507                "Image".to_string(),
508            )),
509            SlackBlock::Image(SlackImageBlock::new(
510                Url::parse("https://example.com/image2.png").unwrap(),
511                "Image2".to_string(),
512            )),
513        ];
514        assert_eq!(
515            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
516            "![Image](https://example.com/image.png)\n![Image2](https://example.com/image2.png)"
517                .to_string()
518        );
519    }
520
521    #[test]
522    fn test_with_divider() {
523        let blocks = vec![
524            SlackBlock::Divider(SlackDividerBlock::new()),
525            SlackBlock::Divider(SlackDividerBlock::new()),
526        ];
527        assert_eq!(
528            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
529            "---\n\n---\n".to_string()
530        );
531    }
532
533    #[test]
534    fn test_with_input() {
535        // No rendering
536        let blocks = vec![SlackBlock::Input(SlackInputBlock::new(
537            "label".into(),
538            SlackInputBlockElement::PlainTextInput(SlackBlockPlainTextInputElement::new(
539                "id".into(),
540            )),
541        ))];
542        assert_eq!(
543            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
544            "".to_string()
545        );
546    }
547
548    #[test]
549    fn test_with_action() {
550        // No rendering
551        let blocks = vec![SlackBlock::Actions(SlackActionsBlock::new(vec![]))];
552        assert_eq!(
553            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
554            "".to_string()
555        );
556    }
557
558    #[test]
559    fn test_with_file() {
560        // No rendering
561        let blocks = vec![SlackBlock::File(SlackFileBlock::new("external_id".into()))];
562        assert_eq!(
563            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
564            "".to_string()
565        );
566    }
567
568    #[test]
569    fn test_with_video() {
570        let blocks = vec![SlackBlock::Video(
571            SlackVideoBlock::new(
572                "alt text".into(),
573                "Video title".into(),
574                "https://example.com/thumbnail.jpg".parse().unwrap(),
575                "https://example.com/video_embed.avi".parse().unwrap(),
576            )
577            .with_description("Video description".into())
578            .with_title_url("https://example.com/video".parse().unwrap()),
579        )];
580        assert_eq!(
581            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
582            r#"*[Video title](https://example.com/video)*
583
584Video description
585
586![alt text](https://example.com/thumbnail.jpg)"#
587                .to_string()
588        );
589    }
590
591    #[test]
592    fn test_with_video_minimal() {
593        let blocks = vec![SlackBlock::Video(SlackVideoBlock::new(
594            "alt text".into(),
595            "Video title".into(),
596            "https://example.com/thumbnail.jpg".parse().unwrap(),
597            "https://example.com/video_embed.avi".parse().unwrap(),
598        ))];
599        assert_eq!(
600            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
601            r#"*Video title*
602
603![alt text](https://example.com/thumbnail.jpg)"#
604                .to_string()
605        );
606    }
607
608    #[test]
609    fn test_with_event() {
610        // No rendering
611        let blocks = vec![SlackBlock::Event(serde_json::json!({}))];
612        assert_eq!(
613            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
614            "".to_string()
615        );
616    }
617
618    #[test]
619    fn test_header() {
620        let blocks = vec![SlackBlock::Header(SlackHeaderBlock::new("Text".into()))];
621        assert_eq!(
622            render_blocks_as_markdown(blocks, SlackReferences::default(), None),
623            "## Text".to_string()
624        );
625    }
626
627    mod section {
628        use super::*;
629
630        #[test]
631        fn test_with_plain_text() {
632            let blocks = vec![
633                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::Plain(
634                    SlackBlockPlainText::new("Text".to_string()),
635                ))),
636                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::Plain(
637                    SlackBlockPlainText::new("Text2".to_string()),
638                ))),
639            ];
640            assert_eq!(
641                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
642                "Text\nText2".to_string()
643            );
644        }
645
646        #[test]
647        fn test_with_markdown() {
648            let blocks = vec![
649                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::MarkDown(
650                    SlackBlockMarkDownText::new("Text".to_string()),
651                ))),
652                SlackBlock::Section(SlackSectionBlock::new().with_text(SlackBlockText::MarkDown(
653                    SlackBlockMarkDownText::new("Text2".to_string()),
654                ))),
655            ];
656            assert_eq!(
657                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
658                "Text\nText2".to_string()
659            );
660        }
661
662        #[test]
663        fn test_with_fields() {
664            let blocks = vec![
665                SlackBlock::Section(SlackSectionBlock::new().with_fields(vec![
666                    SlackBlockText::Plain(SlackBlockPlainText::new("Text11".to_string())),
667                    SlackBlockText::Plain(SlackBlockPlainText::new("Text12".to_string())),
668                ])),
669                SlackBlock::Section(SlackSectionBlock::new().with_fields(vec![
670                    SlackBlockText::Plain(SlackBlockPlainText::new("Text21".to_string())),
671                    SlackBlockText::Plain(SlackBlockPlainText::new("Text22".to_string())),
672                ])),
673            ];
674            assert_eq!(
675                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
676                "Text11Text12\nText21Text22".to_string()
677            );
678        }
679
680        #[test]
681        fn test_with_fields_and_text() {
682            let blocks = vec![
683                SlackBlock::Section(
684                    SlackSectionBlock::new()
685                        .with_text(SlackBlockText::MarkDown(SlackBlockMarkDownText::new(
686                            "Text1".to_string(),
687                        )))
688                        .with_fields(vec![
689                            SlackBlockText::Plain(SlackBlockPlainText::new("Text11".to_string())),
690                            SlackBlockText::Plain(SlackBlockPlainText::new("Text12".to_string())),
691                        ]),
692                ),
693                SlackBlock::Section(
694                    SlackSectionBlock::new()
695                        .with_text(SlackBlockText::MarkDown(SlackBlockMarkDownText::new(
696                            "Text2".to_string(),
697                        )))
698                        .with_fields(vec![
699                            SlackBlockText::Plain(SlackBlockPlainText::new("Text21".to_string())),
700                            SlackBlockText::Plain(SlackBlockPlainText::new("Text22".to_string())),
701                        ]),
702                ),
703            ];
704            assert_eq!(
705                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
706                "Text1Text11Text12\nText2Text21Text22".to_string()
707            );
708        }
709    }
710
711    mod context {
712        use super::*;
713
714        #[test]
715        fn test_with_image() {
716            let blocks = vec![SlackBlock::Context(SlackContextBlock::new(vec![
717                SlackContextBlockElement::Image(SlackBlockImageElement::new(
718                    "https://example.com/image.png".to_string(),
719                    "Image".to_string(),
720                )),
721                SlackContextBlockElement::Image(SlackBlockImageElement::new(
722                    "https://example.com/image2.png".to_string(),
723                    "Image2".to_string(),
724                )),
725            ]))];
726            assert_eq!(
727                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
728                "![Image](https://example.com/image.png)![Image2](https://example.com/image2.png)"
729                    .to_string()
730            );
731        }
732
733        #[test]
734        fn test_with_plain_text() {
735            let blocks = vec![SlackBlock::Context(SlackContextBlock::new(vec![
736                SlackContextBlockElement::Plain(SlackBlockPlainText::new("Text".to_string())),
737                SlackContextBlockElement::Plain(SlackBlockPlainText::new("Text2".to_string())),
738            ]))];
739            assert_eq!(
740                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
741                "TextText2".to_string()
742            );
743        }
744
745        #[test]
746        fn test_with_markdown() {
747            let blocks = vec![SlackBlock::Context(SlackContextBlock::new(vec![
748                SlackContextBlockElement::MarkDown(SlackBlockMarkDownText::new("Text".to_string())),
749                SlackContextBlockElement::MarkDown(SlackBlockMarkDownText::new(
750                    "Text2".to_string(),
751                )),
752            ]))];
753            assert_eq!(
754                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
755                "TextText2".to_string()
756            );
757        }
758    }
759
760    mod rich_text {
761        use super::*;
762
763        #[test]
764        fn test_with_empty_json() {
765            let blocks = vec![
766                SlackBlock::RichText(serde_json::json!({})),
767                SlackBlock::RichText(serde_json::json!({})),
768            ];
769            assert_eq!(
770                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
771                "\n".to_string()
772            );
773        }
774
775        mod rich_text_section {
776            use super::*;
777
778            mod text_element {
779                use super::*;
780
781                #[test]
782                fn test_with_text() {
783                    let blocks = vec![
784                        SlackBlock::RichText(serde_json::json!({
785                            "type": "rich_text",
786                            "elements": [
787                                {
788                                    "type": "rich_text_section",
789                                    "elements": [
790                                        {
791                                            "type": "text",
792                                            "text": "Text111"
793                                        },
794                                        {
795                                            "type": "text",
796                                            "text": "Text112"
797                                        }
798                                    ]
799                                },
800                                {
801                                    "type": "rich_text_section",
802                                    "elements": [
803                                        {
804                                            "type": "text",
805                                            "text": "Text121"
806                                        },
807                                        {
808                                            "type": "text",
809                                            "text": "Text122"
810                                        }
811                                    ]
812                                }
813                            ]
814                        })),
815                        SlackBlock::RichText(serde_json::json!({
816                            "type": "rich_text",
817                            "elements": [
818                                {
819                                    "type": "rich_text_section",
820                                    "elements": [
821                                        {
822                                            "type": "text",
823                                            "text": "Text211"
824                                        },
825                                        {
826                                            "type": "text",
827                                            "text": "Text212"
828                                        }
829                                    ]
830                                },
831                                {
832                                    "type": "rich_text_section",
833                                    "elements": [
834                                        {
835                                            "type": "text",
836                                            "text": "Text221"
837                                        },
838                                        {
839                                            "type": "text",
840                                            "text": "Text222"
841                                        }
842                                    ]
843                                }
844                            ]
845                        })),
846                    ];
847                    assert_eq!(
848                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
849                        "Text111Text112\nText121Text122\nText211Text212\nText221Text222"
850                            .to_string()
851                    );
852                }
853
854                #[test]
855                fn test_with_text_with_newline() {
856                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
857                        "type": "rich_text",
858                        "elements": [
859                            {
860                                "type": "rich_text_section",
861                                "elements": [
862                                    {
863                                        "type": "text",
864                                        "text": "Text111\nText112\n"
865                                    },
866                                    {
867                                        "type": "text",
868                                        "text": "Text211\nText212\n"
869                                    }
870                                ]
871                            }
872                        ]
873                    }))];
874                    assert_eq!(
875                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
876                        "Text111\\\nText112\\\nText211\\\nText212".to_string()
877                    );
878                }
879
880                #[test]
881                fn test_with_text_with_newline_char() {
882                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
883                        "type": "rich_text",
884                        "elements": [
885                            {
886                                "type": "rich_text_section",
887                                "elements": [
888                                    {
889                                        "type": "text",
890                                        "text": "Text111\\nText112"
891                                    }
892                                ]
893                            }
894                        ]
895                    }))];
896                    assert_eq!(
897                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
898                        "Text111\\nText112".to_string()
899                    );
900                }
901
902                #[test]
903                fn test_with_text_with_only_newline() {
904                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
905                        "type": "rich_text",
906                        "elements": [
907                            {
908                                "type": "rich_text_section",
909                                "elements": [
910                                    {
911                                        "type": "text",
912                                        "text": "\n"
913                                    }
914                                ]
915                            }
916                        ]
917                    }))];
918                    assert_eq!(
919                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
920                        "".to_string()
921                    );
922                }
923
924                #[test]
925                fn test_with_bold_text() {
926                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
927                        "type": "rich_text",
928                        "elements": [
929                            {
930                                "type": "rich_text_section",
931                                "elements": [
932                                    {
933                                        "type": "text",
934                                        "text": "Text",
935                                        "style": {
936                                            "bold": true
937                                        }
938                                    }
939                                ]
940                            }
941                        ]
942                    }))];
943                    assert_eq!(
944                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
945                        "*Text*".to_string()
946                    );
947                }
948
949                #[test]
950                fn test_with_consecutive_bold_text() {
951                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
952                        "type": "rich_text",
953                        "elements": [
954                            {
955                                "type": "rich_text_section",
956                                "elements": [
957                                    {
958                                        "type": "text",
959                                        "text": "Hello",
960                                        "style": {
961                                            "bold": true
962                                        }
963                                    },
964                                    {
965                                        "type": "text",
966                                        "text": " ",
967                                        "style": {
968                                            "bold": true
969                                        }
970                                    },
971                                    {
972                                        "type": "text",
973                                        "text": "World!",
974                                        "style": {
975                                            "bold": true
976                                        }
977                                    }
978                                ]
979                            }
980                        ]
981                    }))];
982                    assert_eq!(
983                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
984                        "*Hello World!*".to_string()
985                    );
986                }
987
988                #[test]
989                fn test_with_italic_text() {
990                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
991                        "type": "rich_text",
992                        "elements": [
993                            {
994                                "type": "rich_text_section",
995                                "elements": [
996                                    {
997                                        "type": "text",
998                                        "text": "Text",
999                                        "style": {
1000                                            "italic": true
1001                                        }
1002                                    }
1003                                ]
1004                            }
1005                        ]
1006                    }))];
1007                    assert_eq!(
1008                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1009                        "_Text_".to_string()
1010                    );
1011                }
1012
1013                #[test]
1014                fn test_with_consecutive_italic_text() {
1015                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1016                        "type": "rich_text",
1017                        "elements": [
1018                            {
1019                                "type": "rich_text_section",
1020                                "elements": [
1021                                    {
1022                                        "type": "text",
1023                                        "text": "Hello",
1024                                        "style": {
1025                                            "italic": true
1026                                        }
1027                                    },
1028                                    {
1029                                        "type": "text",
1030                                        "text": " ",
1031                                        "style": {
1032                                            "italic": true
1033                                        }
1034                                    },
1035                                    {
1036                                        "type": "text",
1037                                        "text": "World!",
1038                                        "style": {
1039                                            "italic": true
1040                                        }
1041                                    }
1042                                ]
1043                            }
1044                        ]
1045                    }))];
1046                    assert_eq!(
1047                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1048                        "_Hello World!_".to_string()
1049                    );
1050                }
1051
1052                #[test]
1053                fn test_with_strike_text() {
1054                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1055                        "type": "rich_text",
1056                        "elements": [
1057                            {
1058                                "type": "rich_text_section",
1059                                "elements": [
1060                                    {
1061                                        "type": "text",
1062                                        "text": "Text",
1063                                        "style": {
1064                                            "strike": true
1065                                        }
1066                                    }
1067                                ]
1068                            }
1069                        ]
1070                    }))];
1071                    assert_eq!(
1072                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1073                        "~Text~".to_string()
1074                    );
1075                }
1076
1077                #[test]
1078                fn test_with_consecutive_strike_text() {
1079                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1080                        "type": "rich_text",
1081                        "elements": [
1082                            {
1083                                "type": "rich_text_section",
1084                                "elements": [
1085                                    {
1086                                        "type": "text",
1087                                        "text": "Hello",
1088                                        "style": {
1089                                            "strike": true
1090                                        }
1091                                    },
1092                                    {
1093                                        "type": "text",
1094                                        "text": " ",
1095                                        "style": {
1096                                            "strike": true
1097                                        }
1098                                    },
1099                                    {
1100                                        "type": "text",
1101                                        "text": "World!",
1102                                        "style": {
1103                                            "strike": true
1104                                        }
1105                                    }
1106                                ]
1107                            }
1108                        ]
1109                    }))];
1110                    assert_eq!(
1111                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1112                        "~Hello World!~".to_string()
1113                    );
1114                }
1115
1116                #[test]
1117                fn test_with_code_text() {
1118                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1119                        "type": "rich_text",
1120                        "elements": [
1121                            {
1122                                "type": "rich_text_section",
1123                                "elements": [
1124                                    {
1125                                        "type": "text",
1126                                        "text": "Text",
1127                                        "style": {
1128                                            "code": true
1129                                        }
1130                                    }
1131                                ]
1132                            }
1133                        ]
1134                    }))];
1135                    assert_eq!(
1136                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1137                        "`Text`".to_string()
1138                    );
1139                }
1140
1141                #[test]
1142                fn test_with_consecutive_code_text() {
1143                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1144                        "type": "rich_text",
1145                        "elements": [
1146                            {
1147                                "type": "rich_text_section",
1148                                "elements": [
1149                                    {
1150                                        "type": "text",
1151                                        "text": "Text1",
1152                                        "style": {
1153                                            "code": true
1154                                        }
1155                                    },
1156                                    {
1157                                        "type": "text",
1158                                        "text": "Text2",
1159                                        "style": {
1160                                            "code": true
1161                                        }
1162                                    }
1163                                ]
1164                            }
1165                        ]
1166                    }))];
1167                    assert_eq!(
1168                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1169                        "`Text1Text2`".to_string()
1170                    );
1171                }
1172
1173                #[test]
1174                fn test_with_styled_text() {
1175                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1176                        "type": "rich_text",
1177                        "elements": [
1178                            {
1179                                "type": "rich_text_section",
1180                                "elements": [
1181                                    {
1182                                        "type": "text",
1183                                        "text": "Text",
1184                                        "style": {
1185                                            "bold": true,
1186                                            "italic": true,
1187                                            "strike": true
1188                                        }
1189                                    }
1190                                ]
1191                            }
1192                        ]
1193                    }))];
1194                    assert_eq!(
1195                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1196                        "~_*Text*_~".to_string()
1197                    );
1198                }
1199
1200                #[test]
1201                fn test_with_consecutive_styled_text() {
1202                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1203                        "type": "rich_text",
1204                        "elements": [
1205                            {
1206                                "type": "rich_text_section",
1207                                "elements": [
1208                                    {
1209                                        "name": "chart_with_upwards_trend",
1210                                        "type": "emoji",
1211                                        "style": {
1212                                            "bold": true,
1213                                            "italic": true,
1214                                            "strike": true
1215                                        },
1216                                    },
1217                                    {
1218                                        "type": "text",
1219                                        "text": "Hello",
1220                                        "style": {
1221                                            "bold": true,
1222                                            "italic": true,
1223                                            "strike": true
1224                                        }
1225                                    },
1226                                    {
1227                                        "type": "text",
1228                                        "text": " ",
1229                                        "style": {
1230                                            "bold": true,
1231                                            "italic": true,
1232                                            "strike": true
1233                                        }
1234                                    },
1235                                    {
1236                                        "type": "text",
1237                                        "text": "World!",
1238                                        "style": {
1239                                            "bold": true,
1240                                            "italic": true,
1241                                            "strike": true
1242                                        }
1243                                    }
1244                                ]
1245                            }
1246                        ]
1247                    }))];
1248                    assert_eq!(
1249                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1250                        "~_*📈Hello World!*_~".to_string()
1251                    );
1252                }
1253            }
1254
1255            mod channel_element {
1256                use super::*;
1257
1258                #[test]
1259                fn test_with_channel_id() {
1260                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1261                        "type": "rich_text",
1262                        "elements": [
1263                            {
1264                                "type": "rich_text_section",
1265                                "elements": [
1266                                    {
1267                                        "type": "channel",
1268                                        "channel_id": "C0123456"
1269                                    }
1270                                ]
1271                            }
1272                        ]
1273                    }))];
1274                    assert_eq!(
1275                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1276                        "#C0123456".to_string()
1277                    );
1278                }
1279
1280                #[test]
1281                fn test_with_channel_id_and_reference() {
1282                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1283                        "type": "rich_text",
1284                        "elements": [
1285                            {
1286                                "type": "rich_text_section",
1287                                "elements": [
1288                                    {
1289                                        "type": "channel",
1290                                        "channel_id": "C0123456"
1291                                    }
1292                                ]
1293                            }
1294                        ]
1295                    }))];
1296                    assert_eq!(
1297                        render_blocks_as_markdown(
1298                            blocks,
1299                            SlackReferences {
1300                                channels: HashMap::from([(
1301                                    SlackChannelId("C0123456".to_string()),
1302                                    Some("general".to_string())
1303                                )]),
1304                                ..SlackReferences::default()
1305                            },
1306                            None
1307                        ),
1308                        "#general".to_string()
1309                    );
1310                }
1311            }
1312
1313            mod user_element {
1314                use super::*;
1315
1316                #[test]
1317                fn test_with_user_id() {
1318                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1319                        "type": "rich_text",
1320                        "elements": [
1321                            {
1322                                "type": "rich_text_section",
1323                                "elements": [
1324                                    {
1325                                        "type": "user",
1326                                        "user_id": "user1"
1327                                    }
1328                                ]
1329                            }
1330                        ]
1331                    }))];
1332                    assert_eq!(
1333                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1334                        "@user1".to_string()
1335                    );
1336                }
1337
1338                #[test]
1339                fn test_with_user_id_and_custom_delimiter() {
1340                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1341                        "type": "rich_text",
1342                        "elements": [
1343                            {
1344                                "type": "rich_text_section",
1345                                "elements": [
1346                                    {
1347                                        "type": "user",
1348                                        "user_id": "user1"
1349                                    }
1350                                ]
1351                            }
1352                        ]
1353                    }))];
1354                    assert_eq!(
1355                        render_blocks_as_markdown(
1356                            blocks,
1357                            SlackReferences::default(),
1358                            Some("@".to_string())
1359                        ),
1360                        "@@user1@".to_string()
1361                    );
1362                }
1363
1364                #[test]
1365                fn test_with_user_id_and_reference() {
1366                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1367                        "type": "rich_text",
1368                        "elements": [
1369                            {
1370                                "type": "rich_text_section",
1371                                "elements": [
1372                                    {
1373                                        "type": "user",
1374                                        "user_id": "user1"
1375                                    }
1376                                ]
1377                            }
1378                        ]
1379                    }))];
1380                    assert_eq!(
1381                        render_blocks_as_markdown(
1382                            blocks,
1383                            SlackReferences {
1384                                users: HashMap::from([(
1385                                    SlackUserId("user1".to_string()),
1386                                    Some("John Doe".to_string())
1387                                )]),
1388                                ..SlackReferences::default()
1389                            },
1390                            None
1391                        ),
1392                        "@John Doe".to_string()
1393                    );
1394                }
1395
1396                #[test]
1397                fn test_with_user_id_and_reference_and_custom_delimiter() {
1398                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1399                        "type": "rich_text",
1400                        "elements": [
1401                            {
1402                                "type": "rich_text_section",
1403                                "elements": [
1404                                    {
1405                                        "type": "user",
1406                                        "user_id": "user1"
1407                                    }
1408                                ]
1409                            }
1410                        ]
1411                    }))];
1412                    assert_eq!(
1413                        render_blocks_as_markdown(
1414                            blocks,
1415                            SlackReferences {
1416                                users: HashMap::from([(
1417                                    SlackUserId("user1".to_string()),
1418                                    Some("John Doe".to_string())
1419                                )]),
1420                                ..SlackReferences::default()
1421                            },
1422                            Some("@".to_string())
1423                        ),
1424                        "@@John Doe@".to_string()
1425                    );
1426                }
1427            }
1428
1429            mod usergroup_element {
1430                use super::*;
1431
1432                #[test]
1433                fn test_with_usergroup_id() {
1434                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1435                        "type": "rich_text",
1436                        "elements": [
1437                            {
1438                                "type": "rich_text_section",
1439                                "elements": [
1440                                    {
1441                                        "type": "usergroup",
1442                                        "usergroup_id": "group1"
1443                                    }
1444                                ]
1445                            }
1446                        ]
1447                    }))];
1448                    assert_eq!(
1449                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1450                        "@group1".to_string()
1451                    );
1452                }
1453
1454                #[test]
1455                fn test_with_usergroup_id_and_custom_delimiter() {
1456                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1457                        "type": "rich_text",
1458                        "elements": [
1459                            {
1460                                "type": "rich_text_section",
1461                                "elements": [
1462                                    {
1463                                        "type": "usergroup",
1464                                        "usergroup_id": "group1"
1465                                    }
1466                                ]
1467                            }
1468                        ]
1469                    }))];
1470                    assert_eq!(
1471                        render_blocks_as_markdown(
1472                            blocks,
1473                            SlackReferences::default(),
1474                            Some("@".to_string())
1475                        ),
1476                        "@@group1@".to_string()
1477                    );
1478                }
1479
1480                #[test]
1481                fn test_with_usergroup_id_and_reference() {
1482                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1483                        "type": "rich_text",
1484                        "elements": [
1485                            {
1486                                "type": "rich_text_section",
1487                                "elements": [
1488                                    {
1489                                        "type": "usergroup",
1490                                        "usergroup_id": "group1"
1491                                    }
1492                                ]
1493                            }
1494                        ]
1495                    }))];
1496                    assert_eq!(
1497                        render_blocks_as_markdown(
1498                            blocks,
1499                            SlackReferences {
1500                                usergroups: HashMap::from([(
1501                                    SlackUserGroupId("group1".to_string()),
1502                                    Some("Admins".to_string())
1503                                )]),
1504                                ..SlackReferences::default()
1505                            },
1506                            None
1507                        ),
1508                        "@Admins".to_string()
1509                    );
1510                }
1511
1512                #[test]
1513                fn test_with_usergroup_id_and_reference_and_custom_delimiter() {
1514                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1515                        "type": "rich_text",
1516                        "elements": [
1517                            {
1518                                "type": "rich_text_section",
1519                                "elements": [
1520                                    {
1521                                        "type": "usergroup",
1522                                        "usergroup_id": "group1"
1523                                    }
1524                                ]
1525                            }
1526                        ]
1527                    }))];
1528                    assert_eq!(
1529                        render_blocks_as_markdown(
1530                            blocks,
1531                            SlackReferences {
1532                                usergroups: HashMap::from([(
1533                                    SlackUserGroupId("group1".to_string()),
1534                                    Some("Admins".to_string())
1535                                )]),
1536                                ..SlackReferences::default()
1537                            },
1538                            Some("@".to_string())
1539                        ),
1540                        "@@Admins@".to_string()
1541                    );
1542                }
1543            }
1544
1545            mod link_element {
1546                use super::*;
1547
1548                #[test]
1549                fn test_with_url() {
1550                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1551                        "type": "rich_text",
1552                        "elements": [
1553                            {
1554                                "type": "rich_text_section",
1555                                "elements": [
1556                                    {
1557                                        "type": "link",
1558                                        "text": "example",
1559                                        "url": "https://example.com"
1560                                    }
1561                                ]
1562                            }
1563                        ]
1564                    }))];
1565                    assert_eq!(
1566                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1567                        "[example](https://example.com)".to_string()
1568                    );
1569                }
1570
1571                #[test]
1572                fn test_with_url_without_text() {
1573                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1574                        "type": "rich_text",
1575                        "elements": [
1576                            {
1577                                "type": "rich_text_section",
1578                                "elements": [
1579                                    {
1580                                        "type": "link",
1581                                        "url": "https://example.com"
1582                                    }
1583                                ]
1584                            }
1585                        ]
1586                    }))];
1587                    assert_eq!(
1588                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1589                        "[https://example.com](https://example.com)".to_string()
1590                    );
1591                }
1592            }
1593
1594            mod emoji_element {
1595                use super::*;
1596
1597                #[test]
1598                fn test_with_emoji() {
1599                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1600                        "type": "rich_text",
1601                        "elements": [
1602                            {
1603                                "type": "rich_text_section",
1604                                "elements": [
1605                                    {
1606                                        "type": "emoji",
1607                                        "name": "wave"
1608                                    }
1609                                ]
1610                            }
1611                        ]
1612                    }))];
1613                    assert_eq!(
1614                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1615                        "👋".to_string()
1616                    );
1617                }
1618
1619                #[test]
1620                fn test_with_emoji_with_skin_tone() {
1621                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1622                        "type": "rich_text",
1623                        "elements": [
1624                            {
1625                                "type": "rich_text_section",
1626                                "elements": [
1627                                    {
1628                                        "type": "emoji",
1629                                        "name": "wave::skin-tone-2"
1630                                    }
1631                                ]
1632                            }
1633                        ]
1634                    }))];
1635                    assert_eq!(
1636                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1637                        "👋🏻".to_string()
1638                    );
1639                }
1640
1641                #[test]
1642                fn test_with_emoji_with_unknown_skin_tone() {
1643                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1644                        "type": "rich_text",
1645                        "elements": [
1646                            {
1647                                "type": "rich_text_section",
1648                                "elements": [
1649                                    {
1650                                        "type": "emoji",
1651                                        "name": "wave::skin-tone-42"
1652                                    }
1653                                ]
1654                            }
1655                        ]
1656                    }))];
1657                    assert_eq!(
1658                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1659                        "👋".to_string()
1660                    );
1661                }
1662
1663                #[test]
1664                fn test_with_unknown_emoji() {
1665                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1666                        "type": "rich_text",
1667                        "elements": [
1668                            {
1669                                "type": "rich_text_section",
1670                                "elements": [
1671                                    {
1672                                        "type": "emoji",
1673                                        "name": "unknown1"
1674                                    }
1675                                ]
1676                            }
1677                        ]
1678                    }))];
1679                    assert_eq!(
1680                        render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1681                        ":unknown1:".to_string()
1682                    );
1683                }
1684
1685                #[test]
1686                fn test_with_unknown_emoji_with_slack_reference_alias() {
1687                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1688                        "type": "rich_text",
1689                        "elements": [
1690                            {
1691                                "type": "rich_text_section",
1692                                "elements": [
1693                                    {
1694                                        "type": "emoji",
1695                                        "name": "unknown1"
1696                                    }
1697                                ]
1698                            }
1699                        ]
1700                    }))];
1701                    assert_eq!(
1702                        render_blocks_as_markdown(
1703                            blocks,
1704                            SlackReferences {
1705                                emojis: HashMap::from([(
1706                                    SlackEmojiName("unknown1".to_string()),
1707                                    Some(SlackEmojiRef::Alias(SlackEmojiName("wave".to_string())))
1708                                )]),
1709                                ..SlackReferences::default()
1710                            },
1711                            None
1712                        ),
1713                        "👋".to_string()
1714                    );
1715                }
1716
1717                #[test]
1718                fn test_with_unknown_emoji_with_slack_reference_alias_to_custom_emoji() {
1719                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1720                        "type": "rich_text",
1721                        "elements": [
1722                            {
1723                                "type": "rich_text_section",
1724                                "elements": [
1725                                    {
1726                                        "type": "emoji",
1727                                        "name": "unknown1"
1728                                    }
1729                                ]
1730                            }
1731                        ]
1732                    }))];
1733                    assert_eq!(
1734                        render_blocks_as_markdown(
1735                            blocks,
1736                            SlackReferences {
1737                                emojis: HashMap::from([
1738                                    (
1739                                        SlackEmojiName("unknown1".to_string()),
1740                                        Some(SlackEmojiRef::Alias(SlackEmojiName(
1741                                            "unknown2".to_string()
1742                                        )))
1743                                    ),
1744                                    (
1745                                        SlackEmojiName("unknown2".to_string()),
1746                                        Some(SlackEmojiRef::Url(
1747                                            "https://emoji.com/unknown2.png".parse().unwrap()
1748                                        ))
1749                                    )
1750                                ]),
1751                                ..SlackReferences::default()
1752                            },
1753                            None
1754                        ),
1755                        "![:unknown2:](https://emoji.com/unknown2.png)".to_string()
1756                    );
1757                }
1758
1759                #[test]
1760                fn test_with_unknown_emoji_with_slack_reference_image_url() {
1761                    let blocks = vec![SlackBlock::RichText(serde_json::json!({
1762                        "type": "rich_text",
1763                        "elements": [
1764                            {
1765                                "type": "rich_text_section",
1766                                "elements": [
1767                                    {
1768                                        "type": "emoji",
1769                                        "name": "unknown1"
1770                                    }
1771                                ]
1772                            }
1773                        ]
1774                    }))];
1775                    assert_eq!(
1776                        render_blocks_as_markdown(
1777                            blocks,
1778                            SlackReferences {
1779                                emojis: HashMap::from([(
1780                                    SlackEmojiName("unknown1".to_string()),
1781                                    Some(SlackEmojiRef::Url(
1782                                        "https://emoji.com/unknown1.png".parse().unwrap()
1783                                    ))
1784                                )]),
1785                                ..SlackReferences::default()
1786                            },
1787                            None
1788                        ),
1789                        "![:unknown1:](https://emoji.com/unknown1.png)".to_string()
1790                    );
1791                }
1792            }
1793        }
1794
1795        mod rich_text_list {
1796            use super::*;
1797
1798            #[test]
1799            fn test_with_ordered_list() {
1800                let blocks = vec![SlackBlock::RichText(serde_json::json!({
1801                    "type": "rich_text",
1802                    "elements": [
1803                        {
1804                            "type": "rich_text_list",
1805                            "style": "ordered",
1806                            "elements": [
1807                                {
1808                                    "type": "rich_text_section",
1809                                    "elements": [
1810                                        {
1811                                            "type": "text",
1812                                            "text": "Text1"
1813                                        }
1814                                    ]
1815                                },
1816                                {
1817                                    "type": "rich_text_section",
1818                                    "elements": [
1819                                        {
1820                                            "type": "text",
1821                                            "text": "Text2"
1822                                        }
1823                                    ]
1824                                }
1825                            ]
1826                         },
1827                    ]
1828                }))];
1829                assert_eq!(
1830                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1831                    "1. Text1\n1. Text2".to_string()
1832                );
1833            }
1834
1835            #[test]
1836            fn test_with_nested_ordered_list() {
1837                let blocks = vec![SlackBlock::RichText(serde_json::json!({
1838                    "type": "rich_text",
1839                    "elements": [
1840                        {
1841                            "type": "rich_text_list",
1842                            "style": "ordered",
1843                            "elements": [
1844                                {
1845                                    "type": "rich_text_section",
1846                                    "elements": [
1847                                        {
1848                                            "type": "text",
1849                                            "text": "Text1"
1850                                        }
1851                                    ]
1852                                },
1853                            ]
1854                        },
1855                        {
1856                            "type": "rich_text_list",
1857                            "style": "ordered",
1858                            "elements": [
1859                                {
1860                                    "type": "rich_text_section",
1861                                    "elements": [
1862                                        {
1863                                            "type": "text",
1864                                            "text": "Text2"
1865                                        }
1866                                    ]
1867                                },
1868                            ]
1869                        },
1870                        {
1871                            "type": "rich_text_list",
1872                            "style": "ordered",
1873                            "indent": 1,
1874                            "elements": [
1875                                {
1876                                    "type": "rich_text_section",
1877                                    "elements": [
1878                                        {
1879                                            "type": "text",
1880                                            "text": "Text2.1"
1881                                        }
1882                                    ]
1883                                },
1884                            ]
1885                        },
1886                        {
1887                            "type": "rich_text_list",
1888                            "style": "ordered",
1889                            "indent": 2,
1890                            "elements": [
1891                                {
1892                                    "type": "rich_text_section",
1893                                    "elements": [
1894                                        {
1895                                            "type": "text",
1896                                            "text": "Text2.1.1"
1897                                        }
1898                                    ]
1899                                },
1900                            ]
1901                        }
1902                    ]
1903                }))];
1904                assert_eq!(
1905                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1906                    "1. Text1\n1. Text2\n   1. Text2.1\n      1. Text2.1.1".to_string()
1907                );
1908            }
1909
1910            #[test]
1911            fn test_with_bullet_list() {
1912                let blocks = vec![SlackBlock::RichText(serde_json::json!({
1913                    "type": "rich_text",
1914                    "elements": [
1915                        {
1916                            "type": "rich_text_list",
1917                            "style": "bullet",
1918                            "elements": [
1919                                {
1920                                    "type": "rich_text_section",
1921                                    "elements": [
1922                                        {
1923                                            "type": "text",
1924                                            "text": "Text1"
1925                                        }
1926                                    ]
1927                                },
1928                                {
1929                                    "type": "rich_text_section",
1930                                    "elements": [
1931                                        {
1932                                            "type": "text",
1933                                            "text": "Text2"
1934                                        }
1935                                    ]
1936                                }
1937                            ]
1938                        },
1939                    ]
1940                }))];
1941                assert_eq!(
1942                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
1943                    "- Text1\n- Text2".to_string()
1944                );
1945            }
1946        }
1947
1948        #[test]
1949        fn test_with_nested_bullet_list() {
1950            let blocks = vec![SlackBlock::RichText(serde_json::json!({
1951                "type": "rich_text",
1952                "elements": [
1953                    {
1954                        "type": "rich_text_list",
1955                        "style": "bullet",
1956                        "elements": [
1957                            {
1958                                "type": "rich_text_section",
1959                                "elements": [
1960                                    {
1961                                        "type": "text",
1962                                        "text": "Text1"
1963                                    }
1964                                ]
1965                            },
1966                        ]
1967                    },
1968                    {
1969                        "type": "rich_text_list",
1970                        "style": "bullet",
1971                        "elements": [
1972                            {
1973                                "type": "rich_text_section",
1974                                "elements": [
1975                                    {
1976                                        "type": "text",
1977                                        "text": "Text2"
1978                                    }
1979                                ]
1980                            },
1981                        ]
1982                    },
1983                    {
1984                        "type": "rich_text_list",
1985                        "style": "bullet",
1986                        "indent": 1,
1987                        "elements": [
1988                            {
1989                                "type": "rich_text_section",
1990                                "elements": [
1991                                    {
1992                                        "type": "text",
1993                                        "text": "Text2.1"
1994                                    }
1995                                ]
1996                            },
1997                        ]
1998                    },
1999                    {
2000                        "type": "rich_text_list",
2001                        "style": "bullet",
2002                        "indent": 2,
2003                        "elements": [
2004                            {
2005                                "type": "rich_text_section",
2006                                "elements": [
2007                                    {
2008                                        "type": "text",
2009                                        "text": "Text2.1.1"
2010                                    }
2011                                ]
2012                            },
2013                        ]
2014                    }
2015                ]
2016            }))];
2017            assert_eq!(
2018                render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2019                "- Text1\n- Text2\n  - Text2.1\n    - Text2.1.1".to_string()
2020            );
2021        }
2022
2023        mod rich_text_preformatted {
2024            use super::*;
2025
2026            #[test]
2027            fn test_with_text() {
2028                let blocks = vec![SlackBlock::RichText(serde_json::json!({
2029                    "type": "rich_text",
2030                    "elements": [
2031                        {
2032                            "type": "rich_text_preformatted",
2033                            "elements": [
2034                                {
2035                                    "type": "text",
2036                                    "text": "Text1"
2037                                },
2038                                {
2039                                    "type": "text",
2040                                    "text": "Text2"
2041                                }
2042                            ]
2043                        },
2044                    ]
2045                }))];
2046                assert_eq!(
2047                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2048                    "```\nText1Text2\n```".to_string()
2049                );
2050            }
2051
2052            #[test]
2053            fn test_with_text_and_newline() {
2054                let blocks = vec![SlackBlock::RichText(serde_json::json!({
2055                    "type": "rich_text",
2056                    "elements": [
2057                        {
2058                            "type": "rich_text_preformatted",
2059                            "elements": [
2060                                {
2061                                    "type": "text",
2062                                    "text": "test:\n  sub: value"
2063                                }
2064                            ]
2065                        }
2066                    ]
2067                }))];
2068                assert_eq!(
2069                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2070                    "```\ntest:\n  sub: value\n```".to_string()
2071                );
2072            }
2073
2074            #[test]
2075            fn test_with_preformatted_text_followed_by_text() {
2076                let blocks = vec![SlackBlock::RichText(serde_json::json!({
2077                    "type": "rich_text",
2078                    "elements": [
2079                        {
2080                            "type": "rich_text_preformatted",
2081                            "elements": [
2082                                {
2083                                    "type": "text",
2084                                    "text": "Text1"
2085                                }
2086                            ]
2087                        },
2088                        {
2089                            "type": "rich_text_section",
2090                            "elements": [
2091                                {
2092                                    "type": "text",
2093                                    "text": "Text2"
2094                                }
2095                            ]
2096                        },
2097                    ]
2098                }))];
2099                assert_eq!(
2100                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2101                    "```\nText1\n```\nText2".to_string()
2102                );
2103            }
2104        }
2105
2106        mod rich_text_quote {
2107            use super::*;
2108
2109            #[test]
2110            fn test_with_text() {
2111                let blocks = vec![SlackBlock::RichText(serde_json::json!({
2112                    "type": "rich_text",
2113                    "elements": [
2114                        {
2115                            "type": "rich_text_quote",
2116                            "elements": [
2117                                {
2118                                    "type": "text",
2119                                    "text": "Text1"
2120                                },
2121                                {
2122                                    "type": "text",
2123                                    "text": "Text2"
2124                                }
2125                            ]
2126                        },
2127                    ]
2128                }))];
2129                assert_eq!(
2130                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2131                    "> Text1Text2".to_string()
2132                );
2133            }
2134
2135            #[test]
2136            fn test_with_quoted_text_followed_by_quoted_text() {
2137                let blocks = vec![SlackBlock::RichText(serde_json::json!({
2138                    "type": "rich_text",
2139                    "elements": [
2140                        {
2141                            "type": "rich_text_quote",
2142                            "elements": [
2143                                {
2144                                    "type": "text",
2145                                    "text": "Text1"
2146                                },
2147                            ]
2148                        },
2149                        {
2150                            "type": "rich_text_quote",
2151                            "elements": [
2152                                {
2153                                    "type": "text",
2154                                    "text": "Text2"
2155                                }
2156                            ]
2157                        },
2158                    ]
2159                }))];
2160                assert_eq!(
2161                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2162                    "> Text1\n> Text2".to_string()
2163                );
2164            }
2165
2166            #[test]
2167            fn test_with_quoted_text_followed_by_non_quoted_text() {
2168                let blocks = vec![SlackBlock::RichText(serde_json::json!({
2169                    "type": "rich_text",
2170                    "elements": [
2171                        {
2172                            "type": "rich_text_quote",
2173                            "elements": [
2174                                {
2175                                    "text": "Text1",
2176                                    "type": "text"
2177                                }
2178                            ]
2179                        },
2180                        {
2181                            "type": "rich_text_section",
2182                            "elements": [
2183                                {
2184                                    "text": "Text2",
2185                                    "type": "text"
2186                                },
2187                            ]
2188                        }
2189                    ]
2190                }))];
2191
2192                assert_eq!(
2193                    render_blocks_as_markdown(blocks, SlackReferences::default(), None),
2194                    "> Text1\n\nText2".to_string()
2195                );
2196            }
2197        }
2198    }
2199}