Skip to main content

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