136 lines
3.4 KiB
Rust
136 lines
3.4 KiB
Rust
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<Item = MdItem<'_>> {
|
|
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);
|
|
}
|
|
}
|