Files
inkr/src/custom_code_block.rs
2025-06-12 20:38:50 +02:00

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);
}
}