use nom::{branch::alt,
bytes::complete::{is_not, tag, take_until},
combinator::{map, opt},
sequence::{preceded, terminated},
IResult, Parser};
use crate::{md_parser::constants::{CODE_BLOCK_END, CODE_BLOCK_START_PARTIAL, NEW_LINE},
CodeBlockLine, CodeBlockLineContent, CodeBlockLines, List};
#[rustfmt::skip]
pub fn parse_fenced_code_block(input: &str) -> IResult<&str, List<CodeBlockLine<'_>>> {
let (remainder, (lang, code)) = (
parse_code_block_lang_including_eol,
parse_code_block_body_including_code_block_end,
)
.parse(input)?;
let (remainder, _) = opt(tag(NEW_LINE)).parse(remainder)?;
let acc = split_by_new_line(code);
Ok((remainder, convert_into_code_block_lines(lang, &acc.into())))
}
#[rustfmt::skip]
fn parse_code_block_lang_including_eol(input: &str) -> IResult<&str, Option<&str>> {
alt((
map(
preceded(
tag(CODE_BLOCK_START_PARTIAL),
terminated(
is_not(NEW_LINE),
tag(NEW_LINE),
),
),
Some,
),
map(
(tag(CODE_BLOCK_START_PARTIAL), tag(NEW_LINE)),
|_| None,
),
)).parse(input)
}
#[rustfmt::skip]
fn parse_code_block_body_including_code_block_end(input: &str) -> IResult<&str, &str> {
let (remainder, output) = terminated(
take_until(CODE_BLOCK_END),
tag(CODE_BLOCK_END),
).parse(input)?;
Ok((remainder, output))
}
#[must_use]
pub fn split_by_new_line(input: &str) -> Vec<&str> {
let mut acc: Vec<&str> = input.split('\n').collect();
if let Some(last_item) = acc.last()
&& last_item.is_empty() {
acc.pop();
}
acc
}
fn convert_into_code_block_lines<'a>(
language: Option<&'a str>,
lines: &List<&'a str>,
) -> CodeBlockLines<'a> {
let mut result = List::new();
result.push(CodeBlockLine {
language,
content: CodeBlockLineContent::StartTag,
});
for line in lines.iter() {
result.push(CodeBlockLine {
language,
content: CodeBlockLineContent::Text(line),
});
}
result.push(CodeBlockLine {
language,
content: CodeBlockLineContent::EndTag,
});
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_eq2, list};
#[test]
fn test_parse_codeblock_split_by_eol() {
assert_eq2!(split_by_new_line("foobar\n"), vec!["foobar"]);
assert_eq2!(split_by_new_line("\n"), vec![""]);
assert_eq2!(split_by_new_line(""), Vec::<&str>::new());
assert_eq2!(split_by_new_line("foo\nbar\n"), vec!["foo", "bar"]);
assert_eq2!(split_by_new_line("\nfoo\nbar\n"), vec!["", "foo", "bar"]);
}
#[test]
fn test_parse_codeblock_trailing_extra() {
let input = "```bash\npip install foobar\n````";
let lang = "bash";
let code_lines = vec!["pip install foobar"];
let (remainder, code_block_lines) = parse_fenced_code_block(input).unwrap();
assert_eq2!(remainder, "`");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(Some(lang), &code_lines.into())
);
}
#[test]
fn test_convert_from_code_block_into_lines() {
{
let language = Some("rs");
let lines = vec![];
let expected = list![
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
];
let output = convert_into_code_block_lines(language, &lines.into());
assert_eq2!(output, expected);
}
{
let language = Some("rs");
let lines = vec![""];
let expected = list![
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::Text(""),
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
];
let output = convert_into_code_block_lines(language, &lines.into());
assert_eq2!(output, expected);
}
{
let language = Some("rs");
let lines = vec!["let x = 1;"];
let expected = list![
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::Text("let x = 1;"),
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
];
let output = convert_into_code_block_lines(language, &lines.into());
assert_eq2!(output, expected);
}
{
let language = Some("rs");
let lines = vec!["let x = 1;", "let y = 2;"];
let expected = list![
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::Text("let x = 1;"),
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::Text("let y = 2;"),
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
];
let output = convert_into_code_block_lines(language, &lines.into());
assert_eq2!(output, expected);
}
}
#[test]
fn test_parse_codeblock() {
{
let lang = "bash";
let code_lines = vec!["pip install foobar"];
let input = ["```bash", "pip install foobar", "```", ""].join("\n");
println!("{:#?}", &input);
let (remainder, code_block_lines) = parse_fenced_code_block(&input).unwrap();
assert_eq2!(remainder, "");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(Some(lang), &code_lines.into())
);
}
{
let lang = "bash";
let code_lines = vec![];
let input = ["```bash", "```", ""].join("\n");
let (remainder, code_block_lines) = parse_fenced_code_block(&input).unwrap();
assert_eq2!(remainder, "");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(Some(lang), &code_lines.into())
);
}
{
let lang = "bash";
let code_lines = vec![""];
let input = ["```bash", "", "```", ""].join("\n");
let (remainder, code_block_lines) = parse_fenced_code_block(&input).unwrap();
assert_eq2!(remainder, "");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(Some(lang), &code_lines.into())
);
}
{
let lang = "python";
let code_lines = vec![
"import foobar",
"",
"foobar.pluralize('word') # returns 'words'",
"foobar.pluralize('goose') # returns 'geese'",
"foobar.singularize('phenomena') # returns 'phenomenon'",
];
let input = [
"```python",
"import foobar",
"",
"foobar.pluralize('word') # returns 'words'",
"foobar.pluralize('goose') # returns 'geese'",
"foobar.singularize('phenomena') # returns 'phenomenon'",
"```",
"",
]
.join("\n");
let (remainder, code_block_lines) = parse_fenced_code_block(&input).unwrap();
assert_eq2!(remainder, "");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(Some(lang), &code_lines.into())
);
}
}
#[test]
fn test_parse_codeblock_no_language() {
let lang = None;
let code_lines = vec!["pip install foobar"];
let input = ["```", "pip install foobar", "```", ""].join("\n");
let (remainder, code_block_lines) = parse_fenced_code_block(&input).unwrap();
assert_eq2!(remainder, "");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(lang, &code_lines.into())
);
}
}