use std::{ fmt::{self, Display, Write}, iter, }; const TICKS: &str = "```"; const NL_TICKS: &str = "\n```"; /// Wrap a [Display] in markdown code-block ticks ([TICKS]) pub fn to_custom_code_block(key: &str, content: impl Display) -> String { let mut out = String::new(); write_custom_code_block(&mut out, key, content).unwrap(); out } /// Wrap a [Display] in markdown code-block ticks ([TICKS]) pub fn write_custom_code_block(mut w: impl Write, key: &str, content: impl Display) -> fmt::Result { write!(w, "{TICKS}{key}\n{content}\n{TICKS}") } /// Try to unwrap a string from within markdown code-block ticks ([TICKS]) pub fn try_from_custom_code_block<'a>(key: &str, code_block: &'a str) -> Option<&'a str> { code_block .trim() .strip_prefix(TICKS)? .strip_prefix(key)? .strip_prefix("\n")? .strip_suffix(TICKS)? .strip_suffix("\n") } #[derive(Debug, Clone, Copy)] pub enum MdItem<'a> { /// A line of regular markdown, but not a code block. Line(&'a str), /// A markdown code block CodeBlock { /// The key or language of the code block. key: &'a str, /// Everything in-between the ticks. content: &'a str, /// The entire code-block, including ticks. span: &'a str, }, } /// Iterate over code-blocks in a markdown string pub fn iter_lines_and_code_blocks(mut md: &str) -> impl Iterator> { iter::from_fn(move || { if md.is_empty() { return None; } if !md.starts_with(TICKS) { // line does not start with ticks, return a normal line. let line; if let Some(i) = md.find('\n') { let i = i + 1; line = &md[..i]; md = &md[i..]; } else { line = md; md = ""; } return Some(MdItem::Line(line)); } let mut i = TICKS.len(); let from_key = &md[i..]; let Some((key, from_content)) = from_key.split_once('\n') else { // no more newlines, return the remaining string as the final line. let rest = md; md = ""; return Some(MdItem::Line(rest)); }; i += key.len() + "\n".len(); let Some(end) = from_content.find(NL_TICKS) else { // no closing ticks, return a line instead. let line; if let Some(i) = md.find('\n') { let i = i + 1; line = &md[..i]; md = &md[i..]; } else { line = md; md = ""; } return Some(MdItem::Line(line)); }; let content = &from_content[..end]; i += end + NL_TICKS.len(); if md[i..].starts_with("\n") { i += 1; }; let span = &md[..i]; md = &md[i..]; Some(MdItem::CodeBlock { key, content, span }) }) } #[cfg(test)] mod test { use super::iter_lines_and_code_blocks; #[test] fn iter_markdown() { let markdown = r#" # Hello world ## Subheader - 1 ```foo whatever some code Hi mom! ``` ```` # wrong number of ticks, but that's ok ``` # indented ticks ``` ``` # no closing ticks "#; let list: Vec<_> = iter_lines_and_code_blocks(markdown).collect(); insta::assert_snapshot!(markdown); insta::assert_debug_snapshot!(list); } }