#![deny(missing_docs)]
pub fn strip_markdown(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut in_fence = false;
for line in s.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
in_fence = !in_fence;
continue;
}
if in_fence {
out.push_str(line);
out.push('\n');
continue;
}
let stripped = strip_line(line);
out.push_str(&stripped);
out.push('\n');
}
if !s.ends_with('\n') {
if out.ends_with('\n') {
out.pop();
}
}
out
}
fn strip_line(line: &str) -> String {
let mut s = line.to_string();
s = strip_atx_header(&s);
s = strip_blockquote(&s);
s = strip_list_marker(&s);
s = strip_inline(&s);
s
}
fn strip_atx_header(s: &str) -> String {
let leading_ws: String = s.chars().take_while(|c| c.is_whitespace()).collect();
let rest = &s[leading_ws.len()..];
let mut hashes = 0;
for c in rest.chars().take(6) {
if c == '#' {
hashes += 1;
} else {
break;
}
}
if hashes > 0 && rest[hashes..].starts_with(' ') {
format!("{leading_ws}{}", &rest[hashes + 1..])
} else {
s.to_string()
}
}
fn strip_blockquote(s: &str) -> String {
let leading_ws: String = s.chars().take_while(|c| c.is_whitespace()).collect();
let rest = &s[leading_ws.len()..];
if let Some(stripped) = rest.strip_prefix("> ") {
format!("{leading_ws}{stripped}")
} else if let Some(stripped) = rest.strip_prefix('>') {
format!("{leading_ws}{stripped}")
} else {
s.to_string()
}
}
fn strip_list_marker(s: &str) -> String {
let leading_ws: String = s.chars().take_while(|c| c.is_whitespace()).collect();
let rest = &s[leading_ws.len()..];
if let Some(stripped) = rest.strip_prefix("- ").or(rest.strip_prefix("* ")).or(rest.strip_prefix("+ ")) {
return format!("{leading_ws}{stripped}");
}
let mut digits = 0;
for c in rest.chars() {
if c.is_ascii_digit() {
digits += 1;
} else {
break;
}
}
if digits > 0
&& rest.len() > digits + 1
&& (rest.as_bytes()[digits] == b'.' || rest.as_bytes()[digits] == b')')
&& rest.as_bytes()[digits + 1] == b' '
{
return format!("{leading_ws}{}", &rest[digits + 2..]);
}
s.to_string()
}
fn strip_inline(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if i + 1 < bytes.len() && bytes[i] == b'!' && bytes[i + 1] == b'[' {
if let Some((alt, end)) = parse_link(&s[i + 1..]) {
out.push_str(&alt);
i += 1 + end;
continue;
}
}
if bytes[i] == b'[' {
if let Some((text, end)) = parse_link(&s[i..]) {
out.push_str(&text);
i += end;
continue;
}
}
if bytes[i] == b'`' {
if let Some(end_rel) = s[i + 1..].find('`') {
out.push_str(&s[i + 1..i + 1 + end_rel]);
i += 2 + end_rel;
continue;
}
}
if i + 1 < bytes.len() && (bytes[i] == b'*' && bytes[i + 1] == b'*') {
if let Some(end_rel) = s[i + 2..].find("**") {
out.push_str(&s[i + 2..i + 2 + end_rel]);
i += 4 + end_rel;
continue;
}
}
if i + 1 < bytes.len() && (bytes[i] == b'_' && bytes[i + 1] == b'_') {
if let Some(end_rel) = s[i + 2..].find("__") {
out.push_str(&s[i + 2..i + 2 + end_rel]);
i += 4 + end_rel;
continue;
}
}
if bytes[i] == b'*' {
if let Some(end_rel) = s[i + 1..].find('*') {
out.push_str(&s[i + 1..i + 1 + end_rel]);
i += 2 + end_rel;
continue;
}
}
if bytes[i] == b'_' && is_word_boundary(bytes, i) {
if let Some(end_rel) = s[i + 1..].find('_') {
out.push_str(&s[i + 1..i + 1 + end_rel]);
i += 2 + end_rel;
continue;
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
fn parse_link(s: &str) -> Option<(String, usize)> {
let bytes = s.as_bytes();
if bytes[0] != b'[' {
return None;
}
let close_text = s[1..].find("](")?;
let after_url_off = 1 + close_text + 2;
let close_url = s[after_url_off..].find(')')?;
let text = s[1..1 + close_text].to_string();
Some((text, after_url_off + close_url + 1))
}
fn is_word_boundary(bytes: &[u8], i: usize) -> bool {
if i == 0 {
return true;
}
let prev = bytes[i - 1];
!prev.is_ascii_alphanumeric()
}