Add App
This commit is contained in:
135
src/custom_code_block.rs
Normal file
135
src/custom_code_block.rs
Normal file
@ -0,0 +1,135 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user