storkit: merge 413_refactor_split_slack_rs_into_focused_modules
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
//! 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 regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// 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: ®ex::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(""), "");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user