use nom::{IResult, Parser, branch::alt, combinator::map, multi::many0};
use crate::{List, MdDocument, MdElement,
md_parser::constants::{AUTHORS, DATE, TAGS, TITLE},
parse_block_markdown_text_with_or_without_new_line, parse_csv_opt_eol,
parse_fenced_code_block, parse_heading_in_single_line,
parse_smart_list_block, parse_unique_kv_opt_eol};
#[rustfmt::skip]
pub fn parse_markdown(input: &str) -> IResult<&str, MdDocument<'_>> {
let (input, output) = many0(
alt((
map(parse_title_value, MdElement::Title),
map(parse_tags_list, MdElement::Tags),
map(parse_authors_list, MdElement::Authors),
map(parse_date_value, MdElement::Date),
map(parse_heading_in_single_line, MdElement::Heading),
map(parse_smart_list_block, MdElement::SmartList),
map(parse_fenced_code_block, MdElement::CodeBlock),
map(parse_block_markdown_text_with_or_without_new_line, MdElement::Text),
)),
).parse(input)?;
let it = List::from(output);
Ok((input, it))
}
fn parse_tags_list(input: &str) -> IResult<&str, List<&str>> {
parse_csv_opt_eol(TAGS, input)
}
fn parse_authors_list(input: &str) -> IResult<&str, List<&str>> {
parse_csv_opt_eol(AUTHORS, input)
}
fn parse_title_value(input: &str) -> IResult<&str, &str> {
parse_unique_kv_opt_eol(TITLE, input)
}
fn parse_date_value(input: &str) -> IResult<&str, &str> {
parse_unique_kv_opt_eol(DATE, input)
}
#[cfg(test)]
mod tests_integration_block_smart_lists {
use crate::{MdDocument, PrettyPrintDebug, assert_eq2, parse_markdown};
#[test]
fn test_parse_valid_md_ol_with_indent() {
let input = [
"start",
"1. ol1",
" 2. ol2",
" ol2.1",
" 3. ol3",
" ol3.1",
" ol3.2",
"end",
"",
]
.join("\n");
let expected_output = [
"start",
"[ ┊1.│ol1┊ ]",
"[ ┊ 2.│ol2┊ → ┊ │ol2.1┊ ]",
"[ ┊ 3.│ol3┊ → ┊ │ol3.1┊ → ┊ │ol3.2┊ ]",
"end",
];
let result = parse_markdown(input.as_str());
let remainder = result.as_ref().unwrap().0;
let md_doc: MdDocument<'_> = result.unwrap().1;
assert_eq2!(remainder, "");
md_doc.inner.iter().zip(expected_output.iter()).for_each(
|(element, test_str)| {
let lhs = element.pretty_print_debug();
let rhs = test_str.to_string();
assert_eq2!(lhs, rhs);
},
);
}
#[test]
fn test_parse_valid_md_ul_with_indent() {
let input = [
"start",
"- ul1",
" - ul2",
" ul2.1",
" - ul3",
" ul3.1",
" ul3.2",
"end",
"",
]
.join("\n");
let expected_output = [
"start",
"[ ┊─┤ul1┊ ]",
"[ ┊───┤ul2┊ → ┊ │ul2.1┊ ]",
"[ ┊─────┤ul3┊ → ┊ │ul3.1┊ → ┊ │ul3.2┊ ]",
"end",
];
let result = parse_markdown(input.as_str());
let remainder = result.as_ref().unwrap().0;
let md_doc: MdDocument<'_> = result.unwrap().1;
assert_eq2!(remainder, "");
md_doc.inner.iter().zip(expected_output.iter()).for_each(
|(element, test_str)| {
let lhs = element.pretty_print_debug();
let rhs = test_str.to_string();
assert_eq2!(lhs, rhs);
},
);
}
#[test]
fn test_parse_valid_md_multiline_no_indent() {
let input = vec![
"start",
"- ul1",
"- ul2",
" ul2.1",
" ",
"- ul3",
" ul3.1",
" ul3.2",
"1. ol1",
"2. ol2",
" ol2.1",
"3. ol3",
" ol3.1",
" ol3.2",
"- [ ] todo",
"- [x] done",
"end",
"",
]
.join("\n");
let expected_output = [
"start",
"[ ┊─┤ul1┊ ]",
"[ ┊─┤ul2┊ → ┊ │ul2.1┊ ]",
" ",
"[ ┊─┤ul3┊ → ┊ │ul3.1┊ → ┊ │ul3.2┊ ]",
"[ ┊1.│ol1┊ ]",
"[ ┊2.│ol2┊ → ┊ │ol2.1┊ ]",
"[ ┊3.│ol3┊ → ┊ │ol3.1┊ → ┊ │ol3.2┊ ]",
"[ ┊─┤[ ] todo┊ ]",
"[ ┊─┤[x] done┊ ]",
"end",
];
let result = parse_markdown(input.as_str());
let remainder = result.as_ref().unwrap().0;
let md_doc: MdDocument<'_> = result.unwrap().1;
assert_eq2!(remainder, "");
md_doc.inner.iter().zip(expected_output.iter()).for_each(
|(element, test_str)| {
let lhs = element.pretty_print_debug();
let rhs = test_str.to_string();
assert_eq2!(lhs, rhs);
},
);
}
#[test]
fn test_parse_valid_md_no_indent() {
let input = [
"start",
"- ul1",
"- ul2",
"1. ol1",
"2. ol2",
"- [ ] todo",
"- [x] done",
"end",
"",
]
.join("\n");
let expected_output = [
"start",
"[ ┊─┤ul1┊ ]",
"[ ┊─┤ul2┊ ]",
"[ ┊1.│ol1┊ ]",
"[ ┊2.│ol2┊ ]",
"[ ┊─┤[ ] todo┊ ]",
"[ ┊─┤[x] done┊ ]",
"end",
];
let result = parse_markdown(input.as_str());
let remainder = result.as_ref().unwrap().0;
let md_doc: MdDocument<'_> = result.unwrap().1;
assert_eq2!(remainder, "");
md_doc.inner.iter().zip(expected_output.iter()).for_each(
|(element, test_str)| {
let lhs = element.pretty_print_debug();
let rhs = test_str.to_string();
assert_eq2!(lhs, rhs);
},
);
}
}
#[cfg(test)]
mod tests_parse_markdown {
use super::*;
use crate::{BulletKind, CodeBlockLine, CodeBlockLineContent, HeadingData,
HeadingLevel, HyperlinkData, MdLineFragment, assert_eq2, list};
#[test]
fn test_no_line() {
let input = "Something";
let (remainder, blocks) = parse_markdown(input).unwrap();
println!("remainder: {remainder:?}");
println!("blocks: {blocks:?}");
assert_eq2!(remainder, "");
assert_eq2!(
blocks[0],
MdElement::Text(list![MdLineFragment::Plain("Something")])
);
}
#[test]
fn test_one_line() {
let input = "Something\n";
let (remainder, blocks) = parse_markdown(input).unwrap();
println!("remainder: {remainder:?}");
println!("blocks: {blocks:?}");
assert_eq2!(remainder, "");
assert_eq2!(
blocks[0],
MdElement::Text(list![MdLineFragment::Plain("Something")])
);
}
#[test]
fn test_parse_markdown_with_invalid_text_in_heading() {
let input = ["# LINE 1", "", "##% LINE 2 FOO_BAR:", ""].join("\n");
let (remainder, blocks) = parse_markdown(&input).unwrap();
println!("\nremainder:\n{remainder:?}");
println!("\nblocks:\n{blocks:#?}");
assert_eq2!(remainder, "");
assert_eq2!(blocks.len(), 3);
assert_eq2!(
blocks[0],
MdElement::Heading(HeadingData {
level: HeadingLevel { level: 1 },
text: "LINE 1",
})
);
assert_eq2!(
blocks[1],
MdElement::Text(list![]), );
assert_eq2!(
blocks[2],
MdElement::Text(list![
MdLineFragment::Plain("##% LINE 2 FOO"),
MdLineFragment::Plain("_"),
MdLineFragment::Plain("BAR:"),
])
);
}
#[test]
fn test_parse_markdown_single_line_plain_text() {
let input = ["_this should not be italic", ""].join("\n");
let (remainder, blocks) = parse_markdown(&input).unwrap();
println!("\nremainder:\n{remainder:?}");
println!("\nblocks:\n{blocks:?}");
assert_eq2!(remainder, "");
assert_eq2!(blocks.len(), 1);
assert_eq2!(
blocks[0],
MdElement::Text(list![
MdLineFragment::Plain("_"),
MdLineFragment::Plain("this should not be italic"),
])
);
}
#[test]
fn test_parse_markdown_valid() {
let input = vec to install foobar.",
"```python",
"import foobar",
"",
"foobar.pluralize('word') # returns 'words'",
"foobar.pluralize('goose') # returns 'geese'",
"foobar.singularize('phenomena') # returns 'phenomenon'",
"```",
"- ul1",
"- ul2",
"1. ol1",
"2. ol2",
"- [ ] todo",
"- [x] done",
"end",
"",
]
.join("\n");
let (remainder, list_block) = parse_markdown(&input).unwrap();
let vec_block = &[
MdElement::Title("Something"),
MdElement::Tags(list!["tag1", "tag2", "tag3"]),
MdElement::Heading(HeadingData {
level: HeadingLevel { level: 1 },
text: "Foobar",
}),
MdElement::Text(list![]),
MdElement::Text(list![MdLineFragment::Plain(
"Foobar is a Python library for dealing with word pluralization.",
)]),
MdElement::Text(list![]),
MdElement::CodeBlock(list![
CodeBlockLine {
language: Some("bash"),
content: CodeBlockLineContent::StartTag
},
CodeBlockLine {
language: Some("bash"),
content: CodeBlockLineContent::Text("pip install foobar")
},
CodeBlockLine {
language: Some("bash"),
content: CodeBlockLineContent::EndTag
},
]),
MdElement::CodeBlock(list![
CodeBlockLine {
language: Some("fish"),
content: CodeBlockLineContent::StartTag
},
CodeBlockLine {
language: Some("fish"),
content: CodeBlockLineContent::EndTag
},
]),
MdElement::CodeBlock(list![
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::StartTag
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::Text("")
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::EndTag
},
]),
MdElement::Heading(HeadingData {
level: HeadingLevel { level: 2 },
text: "Installation",
}),
MdElement::Text(list![]),
MdElement::Text(list![
MdLineFragment::Plain("Use the package manager "),
MdLineFragment::Link(HyperlinkData::from((
"pip",
"https://pip.pypa.io/en/stable/",
))),
MdLineFragment::Plain(" to install foobar."),
]),
MdElement::CodeBlock(list![
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::StartTag
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::Text("import foobar")
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::Text("")
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::Text(
"foobar.pluralize('word') # returns 'words'"
)
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::Text(
"foobar.pluralize('goose') # returns 'geese'"
)
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::Text(
"foobar.singularize('phenomena') # returns 'phenomenon'"
)
},
CodeBlockLine {
language: Some("python"),
content: CodeBlockLineContent::EndTag
},
]),
MdElement::SmartList((
list![list![
MdLineFragment::UnorderedListBullet {
indent: 0,
is_first_line: true
},
MdLineFragment::Plain("ul1"),
],],
BulletKind::Unordered,
0,
)),
MdElement::SmartList((
list![list![
MdLineFragment::UnorderedListBullet {
indent: 0,
is_first_line: true
},
MdLineFragment::Plain("ul2"),
],],
BulletKind::Unordered,
0,
)),
MdElement::SmartList((
list![list![
MdLineFragment::OrderedListBullet {
indent: 0,
number: 1,
is_first_line: true
},
MdLineFragment::Plain("ol1"),
],],
BulletKind::Ordered(1),
0,
)),
MdElement::SmartList((
list![list![
MdLineFragment::OrderedListBullet {
indent: 0,
number: 2,
is_first_line: true
},
MdLineFragment::Plain("ol2"),
],],
BulletKind::Ordered(2),
0,
)),
MdElement::SmartList((
list![list![
MdLineFragment::UnorderedListBullet {
indent: 0,
is_first_line: true
},
MdLineFragment::Checkbox(false),
MdLineFragment::Plain(" todo"),
],],
BulletKind::Unordered,
0,
)),
MdElement::SmartList((
list![list![
MdLineFragment::UnorderedListBullet {
indent: 0,
is_first_line: true
},
MdLineFragment::Checkbox(true),
MdLineFragment::Plain(" done"),
],],
BulletKind::Unordered,
0,
)),
MdElement::Text(list![MdLineFragment::Plain("end")]),
];
assert_eq2!(remainder, "");
let size_left = list_block.len();
let size_right = vec_block.len();
assert_eq2!(size_left, size_right);
list_block
.iter()
.zip(vec_block.iter())
.for_each(|(lhs, rhs)| assert_eq2!(lhs, rhs));
}
#[test]
fn test_markdown_invalid() {
let input = [
"@tags: [foo, bar",
"",
"```rs",
"let a=1;",
"```",
"",
"*italic* **bold** [link](https://example.com)",
"",
"`inline code`",
]
.join("\n");
let (remainder, blocks) = parse_markdown(&input).unwrap();
assert_eq2!(remainder, "");
assert_eq2!(blocks.len(), 7);
}
}