use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
pub struct MarkdownString(pub String);
impl Display for MarkdownString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub struct MarkdownEscaped<'a>(pub &'a str);
pub struct MarkdownInlineCode<'a>(pub &'a str);
pub struct MarkdownCodeBlock<'a> {
pub tag: &'a str,
pub text: &'a str,
}
impl Display for MarkdownEscaped<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut start_of_unescaped = None;
for (ix, c) in self.0.char_indices() {
match c {
'\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' |
'#' | '+' | '=' | '-' => {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
write!(formatter, "\\")?;
start_of_unescaped = Some(ix);
}
'<' => {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
write!(formatter, "<")?;
start_of_unescaped = None;
}
'>' => {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
write!(formatter, ">")?;
start_of_unescaped = None;
}
_ => {
if start_of_unescaped.is_none() {
start_of_unescaped = Some(ix);
}
}
}
}
if let Some(start_of_unescaped) = start_of_unescaped {
write!(formatter, "{}", &self.0[start_of_unescaped..])?;
}
Ok(())
}
}
impl Display for MarkdownInlineCode<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut all_whitespace = true;
let text = self
.0
.chars()
.map(|c| {
if c.is_whitespace() {
' '
} else {
all_whitespace = false;
c
}
})
.collect::<String>();
if all_whitespace {
write!(formatter, "`{text}`")
} else {
let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1);
let space = match text.as_bytes() {
&[b'`', ..] | &[.., b'`'] => " ", &[b' ', .., b' '] => " ", _ => "", };
write!(formatter, "{backticks}{space}{text}{space}{backticks}")
}
}
}
impl Display for MarkdownCodeBlock<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let tag = self.tag;
let text = self.text;
let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1));
write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n")
}
}
fn count_max_consecutive_chars(text: &str, search: char) -> usize {
let mut in_search_chars = false;
let mut max_count = 0;
let mut cur_count = 0;
for ch in text.chars() {
if ch == search {
cur_count += 1;
in_search_chars = true;
} else if in_search_chars {
max_count = max_count.max(cur_count);
cur_count = 0;
in_search_chars = false;
}
}
max_count.max(cur_count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markdown_escaped() {
let input = r#"
# Heading
Another heading
===
Another heading variant
---
Paragraph with [link](https://example.com) and `code`, *emphasis*, and ~strikethrough~.
```
code block
```
List with varying leaders:
- Item 1
* Item 2
+ Item 3
Some math: $`\sqrt{3x-1}+(1+x)^2`$
HTML entity:
"#;
let expected = r#"
\# Heading
Another heading
\=\=\=
Another heading variant
\-\-\-
Paragraph with \[link](https://example.com) and \`code\`, \*emphasis\*, and \~strikethrough\~.
\`\`\`
code block
\`\`\`
List with varying leaders:
\- Item 1
\* Item 2
\+ Item 3
Some math: \$\`\\sqrt{3x\-1}\+(1\+x)\^2\`\$
HTML entity: \
"#;
assert_eq!(MarkdownEscaped(input).to_string(), expected);
}
#[test]
fn test_markdown_inline_code() {
assert_eq!(MarkdownInlineCode(" ").to_string(), "` `");
assert_eq!(MarkdownInlineCode("text").to_string(), "`text`");
assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `");
assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `");
assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``");
assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```");
assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``");
assert_eq!(
MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(),
"``some `text` no leading or trailing backticks``"
);
}
#[test]
fn test_count_max_consecutive_chars() {
assert_eq!(
count_max_consecutive_chars("``a```b``", '`'),
3,
"the highest seen consecutive segment of backticks counts"
);
assert_eq!(
count_max_consecutive_chars("```a``b`", '`'),
3,
"it can't be downgraded later"
);
}
}