use crate::{CodeBlockLine, CodeBlockLineContent, CodeBlockLines,
md_parser::md_parser_constants::{CODE_BLOCK_END, CODE_BLOCK_START_PARTIAL,
NEW_LINE, NEWLINE_OR_NULL, NULL_CHAR},
parse_null_padded_line::{is, trim_optional_leading_newline_and_nulls}};
use nom::{IResult, Parser,
branch::alt,
bytes::complete::{is_not, tag, take_until, take_while},
combinator::{eof, map},
multi::many0,
sequence::{preceded, terminated}};
#[rustfmt::skip]
pub fn parse_fenced_code_block(input: &str) -> IResult<&str, CodeBlockLines<'_>> {
let (remainder, (lang, code)) = (
parse_code_block_lang_including_eol,
parse_code_block_body_including_code_block_end,
)
.parse(input)?;
let remainder = trim_optional_leading_newline_and_nulls(remainder);
let acc = split_by_new_line(code);
Ok((remainder, convert_into_code_block_lines(lang, &acc)))
}
#[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(NEWLINE_OR_NULL),
(tag(NEW_LINE), take_while(is(NULL_CHAR))),
),
),
Some,
),
map(
(
tag(CODE_BLOCK_START_PARTIAL),
tag(NEW_LINE),
take_while(is(NULL_CHAR))
),
|_| 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 parser = alt((
terminated(
map(is_not(NEWLINE_OR_NULL), |s: &str| s),
(
tag(NEW_LINE),
take_while(is(NULL_CHAR)),
),
),
map(
(
tag(NEW_LINE),
take_while(is(NULL_CHAR)),
),
|_| "",
),
terminated(
is_not(NEWLINE_OR_NULL),
eof,
),
));
let result: IResult<&str, Vec<&str>> = many0(parser).parse(input);
match result {
Ok((_, lines)) => lines,
Err(_) => Vec::new(), }
}
fn convert_into_code_block_lines<'a>(
language: Option<&'a str>,
lines: &[&'a str],
) -> CodeBlockLines<'a> {
let mut result = CodeBlockLines::new();
result.push(CodeBlockLine {
language,
content: CodeBlockLineContent::StartTag,
});
for line in lines {
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;
#[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)
);
}
#[test]
fn test_convert_from_code_block_into_lines() {
{
let language = Some("rs");
let lines = vec![];
let expected = [
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
]
.into();
let output = convert_into_code_block_lines(language, &lines);
assert_eq2!(output, expected);
}
{
let language = Some("rs");
let lines = vec![""];
let expected = [
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::Text(""),
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
]
.into();
let output = convert_into_code_block_lines(language, &lines);
assert_eq2!(output, expected);
}
{
let language = Some("rs");
let lines = vec!["let x = 1;"];
let expected = [
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::StartTag,
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::Text("let x = 1;"),
},
CodeBlockLine {
language: Some("rs"),
content: CodeBlockLineContent::EndTag,
},
]
.into();
let output = convert_into_code_block_lines(language, &lines);
assert_eq2!(output, expected);
}
{
let language = Some("rs");
let lines = vec!["let x = 1;", "let y = 2;"];
let expected = [
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,
},
]
.into();
let output = convert_into_code_block_lines(language, &lines);
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)
);
}
{
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)
);
}
{
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)
);
}
{
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)
);
}
}
#[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)
);
}
#[test]
fn test_parse_codeblock_with_null_padding() {
{
let lang = "python";
let code_lines = vec!["import foo", "bar()"];
let input = "```python\nimport foo\nbar()\n```\n\0\0\0";
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)
);
}
{
let lang = "bash";
let code_lines = vec!["pip install foobar"];
let input = "```bash\npip install foobar\n```\0\0\0\nNext line";
let (remainder, code_block_lines) = parse_fenced_code_block(input).unwrap();
assert_eq2!(remainder, "\0\0\0\nNext line");
assert_eq2!(
code_block_lines,
convert_into_code_block_lines(Some(lang), &code_lines)
);
}
}
}