1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Convert standard Markdown to Slack mrkdwn format.
//!
//! Slack uses its own "mrkdwn" syntax that differs from standard Markdown:
//! - Bold: `*text*` (not `**text**`)
//! - Italic: `_text_` (same)
//! - Strikethrough: `~text~` (not `~~text~~`)
//! - Code: `` `code` `` (same)
//! - Code blocks: ``` (same)
//! - Links: `<url|text>` (not `[text](url)`)
//! - Headings: not supported — convert to bold text
/// Convert Markdown text to Slack mrkdwn format.
pub fn markdown_to_mrkdwn(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
let mut in_code_block = false;
let mut in_inline_code = false;
let mut line_start = true;
while let Some(c) = chars.next() {
// Track code blocks — don't convert inside them
if c == '`' && chars.peek() == Some(&'`') {
let next = chars.next(); // second `
if chars.peek() == Some(&'`') {
chars.next(); // third `
in_code_block = !in_code_block;
output.push_str("```");
continue;
}
// Two backticks — put them back
output.push('`');
if let Some(n) = next {
output.push(n);
}
continue;
}
if in_code_block {
output.push(c);
line_start = c == '\n';
continue;
}
// Inline code — don't convert inside
if c == '`' {
in_inline_code = !in_inline_code;
output.push(c);
continue;
}
if in_inline_code {
output.push(c);
continue;
}
// Headings at line start: # Heading → *Heading*
if line_start && c == '#' {
while chars.peek() == Some(&'#') {
chars.next();
}
// Skip space after #
if chars.peek() == Some(&' ') {
chars.next();
}
// Collect rest of line
let mut heading = String::new();
for hc in chars.by_ref() {
if hc == '\n' {
break;
}
heading.push(hc);
}
let heading = heading.trim_end();
output.push_str(&format!("*{}*\n", heading));
line_start = true;
continue;
}
// Bold: **text** → *text*
if c == '*' && chars.peek() == Some(&'*') {
chars.next(); // consume second *
output.push('*');
continue;
}
// Strikethrough: ~~text~~ → ~text~
if c == '~' && chars.peek() == Some(&'~') {
chars.next(); // consume second ~
output.push('~');
continue;
}
// Links: [text](url) → <url|text>
if c == '[' {
let mut text = String::new();
let mut found_close = false;
// Collect link text
for lc in chars.by_ref() {
if lc == ']' {
found_close = true;
break;
}
text.push(lc);
}
if found_close && chars.peek() == Some(&'(') {
chars.next(); // consume (
let mut url = String::new();
for uc in chars.by_ref() {
if uc == ')' {
break;
}
url.push(uc);
}
output.push_str(&format!("<{}|{}>", url, text));
} else {
// Not a link, output as-is
output.push('[');
output.push_str(&text);
if found_close {
output.push(']');
}
}
continue;
}
line_start = c == '\n';
output.push(c);
}
output
}