use nom::{bytes::complete::tag,
combinator::opt,
sequence::preceded,
IResult,
Parser as _};
use crate::{list,
md_parser::constants::{COLON, COMMA, NEW_LINE, SPACE},
take_text_until_new_line_or_end,
InlineVec,
List};
pub fn parse_csv_opt_eol<'a>(
tag_name: &'a str,
input: &'a str,
) -> IResult<&'a str, List<&'a str>> {
let (remainder, tags_text) = preceded(
(tag(tag_name), tag(COLON), tag(SPACE)),
take_text_until_new_line_or_end(),
)
.parse(input)?;
let (remainder, _) = opt(tag(NEW_LINE)).parse(remainder)?;
if tags_text.is_empty() {
Ok((remainder, list![]))
}
else {
let (_, vec_tags_text) = parse_comma_separated_list(tags_text)?;
Ok((remainder, List::from(vec_tags_text)))
}
}
fn parse_comma_separated_list(input: &str) -> IResult<&str, InlineVec<&str>> {
let acc: InlineVec<&str> = input.split(COMMA).collect();
let mut trimmed_acc: InlineVec<&str> = InlineVec::with_capacity(acc.len());
match acc.len() {
0 => {
}
1 => {
let only_item = &acc[0];
if only_item.starts_with(SPACE) {
return Err(nom::Err::Error(nom::error::Error::new(
"Only item must not start with space.",
nom::error::ErrorKind::Fail,
)));
} else {
trimmed_acc.push(only_item);
}
}
_ => {
let mut my_iter = acc.iter();
let first_item = my_iter.next().unwrap();
if first_item.starts_with(SPACE) {
return Err(nom::Err::Error(nom::error::Error::new(
"First item must not start with space.",
nom::error::ErrorKind::Fail,
)));
} else {
trimmed_acc.push(first_item);
}
for rest_item in my_iter {
if !rest_item.starts_with(SPACE) {
return Err(nom::Err::Error(nom::error::Error::new(
"Non-first item must start with space.",
nom::error::ErrorKind::Fail,
)));
}
trimmed_acc.push(&rest_item[1..]);
}
}
}
Ok((input, trimmed_acc))
}
#[cfg(test)]
mod test_parse_tags_opt_eol {
use super::*;
use crate::{assert_eq2, md_parser::constants::TAGS};
#[test]
fn test_not_quoted_no_eol() {
let input = "@tags: tag1, tag2, tag3";
let (input, output) = super::parse_csv_opt_eol(TAGS, input).unwrap();
assert_eq2!(input, "");
assert_eq2!(output, list!["tag1", "tag2", "tag3"]);
}
#[test]
fn test_not_quoted_no_eol_err_whitespace() {
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2, tag3").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1,tag2, tag3").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2,tag3").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2,tag3").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2, tag3").unwrap(),
("", list!["tag1", "tag2", " tag3"]),
);
}
#[test]
fn test_not_quoted_with_eol() {
{
let input = "@tags: tag1, tag2, tag3\n";
let (input, output) = parse_csv_opt_eol(TAGS, input).unwrap();
assert_eq2!(input, "");
assert_eq2!(output, list!["tag1", "tag2", "tag3"]);
}
{
let input = "@tags: tag1, tag2, tag3\n]\n";
let result = parse_csv_opt_eol(TAGS, input);
assert_eq2!(result.is_err(), false);
}
{
let input = "@tags: tag1, tag2, tag3";
let result = parse_csv_opt_eol(TAGS, input);
assert_eq2!(result.is_err(), false);
}
}
#[test]
fn test_not_quoted_with_eol_whitespace() {
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2, tag3\n").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1,tag2, tag3\n").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2,tag3\n").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2,tag3\n").is_err(),
true,
);
assert_eq2!(
parse_csv_opt_eol(TAGS, "@tags: tag1, tag2, tag3\n").unwrap(),
("", list!["tag1", "tag2", " tag3"]),
);
}
#[test]
fn test_not_quoted_with_postfix_content() {
let input = "@tags: \nfoo\nbar";
let (input, output) = parse_csv_opt_eol(TAGS, input).unwrap();
assert_eq2!(input, "foo\nbar");
assert_eq2!(output, list![]);
}
}