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