use crate::markdown::options::MarkdownOptions;
use crate::markdown::state::ConversionState;
pub fn convert_strong(inner: &str, opts: &MarkdownOptions, _state: &ConversionState) -> String {
let trimmed = inner.trim();
if trimmed.is_empty() {
return String::new();
}
format!(
"{}{}{}",
opts.strong_delimiter, trimmed, opts.strong_delimiter
)
}
pub fn convert_emphasis(inner: &str, opts: &MarkdownOptions, _state: &ConversionState) -> String {
let trimmed = inner.trim();
if trimmed.is_empty() {
return String::new();
}
format!(
"{}{}{}",
opts.emphasis_delimiter, trimmed, opts.emphasis_delimiter
)
}
pub fn convert_inline_code(
inner: &str,
_opts: &MarkdownOptions,
_state: &ConversionState,
) -> String {
let trimmed = inner.trim();
if trimmed.is_empty() {
return String::new();
}
if trimmed.contains('`') {
format!("`` {} ``", trimmed)
} else {
format!("`{}`", trimmed)
}
}
pub fn convert_strikethrough(
inner: &str,
_opts: &MarkdownOptions,
_state: &ConversionState,
) -> String {
let trimmed = inner.trim();
if trimmed.is_empty() {
return String::new();
}
format!("~~{}~~", trimmed)
}
pub fn convert_highlight(inner: &str, _opts: &MarkdownOptions, _state: &ConversionState) -> String {
let trimmed = inner.trim();
if trimmed.is_empty() {
return String::new();
}
format!("=={}==", trimmed)
}
pub fn convert_br() -> String {
" \n".to_string()
}
pub fn convert_hr() -> String {
"\n\n---\n\n".to_string()
}
pub fn escape_markdown(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' | '`' | '*' | '_' | '~' => {
result.push('\\');
result.push(ch);
}
_ => result.push(ch),
}
}
result
}
pub(crate) fn escape_link_text(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' | '[' | ']' => {
result.push('\\');
result.push(ch);
}
_ => result.push(ch),
}
}
result
}
pub(crate) fn escape_link_brackets(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'[' | ']' => {
result.push('\\');
result.push(ch);
}
_ => result.push(ch),
}
}
result
}
pub(crate) fn escape_url_destination(url: &str) -> String {
let stripped: String = url.chars().filter(|c| !c.is_control()).collect();
let needs_wrap = stripped
.chars()
.any(|c| matches!(c, ' ' | '(' | ')' | '<' | '>'));
if !needs_wrap {
return stripped;
}
let mut inner = String::with_capacity(stripped.len());
for ch in stripped.chars() {
match ch {
'<' => inner.push_str("%3C"),
'>' => inner.push_str("%3E"),
_ => inner.push(ch),
}
}
format!("<{}>", inner)
}
pub(crate) fn escape_md_title(title: &str) -> String {
let mut result = String::with_capacity(title.len());
for ch in title.chars() {
match ch {
'\n' | '\r' => {}
'\\' | '"' => {
result.push('\\');
result.push(ch);
}
_ => result.push(ch),
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strong() {
let opts = MarkdownOptions::default();
let state = ConversionState::default();
assert_eq!(convert_strong("bold", &opts, &state), "**bold**");
}
#[test]
fn test_emphasis() {
let opts = MarkdownOptions::default();
let state = ConversionState::default();
assert_eq!(convert_emphasis("italic", &opts, &state), "*italic*");
}
#[test]
fn test_inline_code_with_backticks() {
let opts = MarkdownOptions::default();
let state = ConversionState::default();
assert_eq!(convert_inline_code("a`b", &opts, &state), "`` a`b ``");
}
#[test]
fn test_escape_markdown() {
assert_eq!(escape_markdown("hello *world*"), "hello \\*world\\*");
}
#[test]
fn test_escape_link_text_benign_unchanged() {
assert_eq!(escape_link_text("A nice photo"), "A nice photo");
}
#[test]
fn test_escape_link_text_brackets_escaped() {
assert_eq!(
escape_link_text("](javascript:evil()) [x"),
"\\](javascript:evil()) \\[x"
);
}
#[test]
fn test_escape_link_text_backslash_first() {
assert_eq!(escape_link_text("a\\[b"), "a\\\\\\[b");
}
#[test]
fn test_escape_url_destination_benign_unchanged() {
assert_eq!(
escape_url_destination("https://example.com/a?b=c#d"),
"https://example.com/a?b=c#d"
);
}
#[test]
fn test_escape_url_destination_wraps_on_paren() {
assert_eq!(
escape_url_destination("http://e.com/a)b"),
"<http://e.com/a)b>"
);
}
#[test]
fn test_escape_url_destination_wraps_on_space() {
assert_eq!(
escape_url_destination("http://e.com/a b"),
"<http://e.com/a b>"
);
}
#[test]
fn test_escape_url_destination_percent_encodes_angles_when_wrapped() {
assert_eq!(
escape_url_destination("http://e.com/(a<b>"),
"<http://e.com/(a%3Cb%3E>"
);
}
#[test]
fn test_escape_url_destination_strips_control_chars() {
assert_eq!(
escape_url_destination("java\tscript:alert(1)"),
"<javascript:alert(1)>"
);
}
#[test]
fn test_escape_md_title_quote_and_backslash() {
assert_eq!(escape_md_title("a\"b\\c"), "a\\\"b\\\\c");
}
#[test]
fn test_escape_md_title_strips_newlines() {
assert_eq!(escape_md_title("a\nb\rc"), "abc");
}
}