Files
huskies/server/src/chat/transport/slack/format.rs
T
dave 845b85e7a7 fix: add --all to cargo fmt in script/test and autoformat codebase
cargo fmt without --all fails with "Failed to find targets" in
workspace repos. This was blocking every story's gates. Also ran
cargo fmt --all to fix all existing formatting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:07:08 +00:00

137 lines
4.6 KiB
Rust

//! Markdown to Slack mrkdwn conversion.
/// Convert Markdown text to Slack mrkdwn format.
///
/// Slack uses its own "mrkdwn" syntax which differs from standard Markdown.
/// This function converts common Markdown constructs so messages render
/// nicely in Slack instead of showing raw Markdown syntax.
pub fn markdown_to_slack(text: &str) -> String {
use crate::chat::util::normalize_line_breaks;
use regex::Regex;
use std::sync::LazyLock;
let normalized = normalize_line_breaks(text);
let text = normalized.as_str();
// Regexes are compiled once and reused across calls.
static RE_FENCED_BLOCK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?ms)^```.*?\n(.*?)^```").unwrap());
static RE_HEADER: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^#{1,6}\s+(.+)$").unwrap());
static RE_BOLD_ITALIC: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\*\*\*(.+?)\*\*\*").unwrap());
static RE_BOLD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*\*(.+?)\*\*").unwrap());
static RE_STRIKETHROUGH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"~~(.+?)~~").unwrap());
static RE_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap());
// 1. Protect fenced code blocks by replacing them with placeholders.
let mut code_blocks: Vec<String> = Vec::new();
let protected = RE_FENCED_BLOCK.replace_all(text, |caps: &regex::Captures| {
let idx = code_blocks.len();
code_blocks.push(caps[0].to_string());
format!("\x00CODEBLOCK{idx}\x00")
});
let mut out = protected.into_owned();
// 2. Headers → bold text.
out = RE_HEADER.replace_all(&out, "*$1*").into_owned();
// 3. Bold+italic (***text***) → bold italic (*_text_*).
out = RE_BOLD_ITALIC.replace_all(&out, "*_${1}_*").into_owned();
// 4. Bold (**text**) → Slack bold (*text*).
out = RE_BOLD.replace_all(&out, "*$1*").into_owned();
// 5. Strikethrough (~~text~~) → Slack strikethrough (~text~).
out = RE_STRIKETHROUGH.replace_all(&out, "~$1~").into_owned();
// 6. Links [text](url) → Slack mrkdwn format <url|text>.
out = RE_LINK.replace_all(&out, "<$2|$1>").into_owned();
// 7. Restore code blocks.
for (idx, block) in code_blocks.iter().enumerate() {
out = out.replace(&format!("\x00CODEBLOCK{idx}\x00"), block);
}
out
}
// ── Tests ───────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slack_headers_become_bold() {
assert_eq!(markdown_to_slack("# Title"), "*Title*");
assert_eq!(markdown_to_slack("## Subtitle"), "*Subtitle*");
assert_eq!(markdown_to_slack("### Section"), "*Section*");
assert_eq!(markdown_to_slack("###### Deep"), "*Deep*");
}
#[test]
fn slack_bold_converted() {
assert_eq!(markdown_to_slack("**bold text**"), "*bold text*");
}
#[test]
fn slack_bold_italic_converted() {
assert_eq!(markdown_to_slack("***emphasis***"), "*_emphasis_*");
}
#[test]
fn slack_strikethrough_converted() {
assert_eq!(markdown_to_slack("~~removed~~"), "~removed~");
}
#[test]
fn slack_links_converted_to_mrkdwn() {
assert_eq!(
markdown_to_slack("[click here](https://example.com)"),
"<https://example.com|click here>"
);
}
#[test]
fn slack_inline_code_preserved() {
assert_eq!(markdown_to_slack("use `foo()` here"), "use `foo()` here");
}
#[test]
fn slack_fenced_code_block_preserved() {
let input = "```rust\nlet x = 1;\n```";
let output = markdown_to_slack(input);
assert!(
output.contains("let x = 1;"),
"code block content must be preserved"
);
assert!(
output.contains("```"),
"fenced code delimiters must be preserved"
);
}
#[test]
fn slack_code_block_content_not_transformed() {
let input = "```\n**not bold** # not header\n```";
let output = markdown_to_slack(input);
assert!(
output.contains("**not bold**"),
"markdown inside code blocks must not be transformed"
);
}
#[test]
fn slack_plain_text_unchanged() {
let plain = "Hello, this is a plain message with no formatting.";
assert_eq!(markdown_to_slack(plain), plain);
}
#[test]
fn slack_empty_string_unchanged() {
assert_eq!(markdown_to_slack(""), "");
}
}