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