use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::anychar; use nom::character::complete::digit1; use nom::character::complete::line_ending; use nom::character::complete::one_of; use nom::character::complete::space0; use nom::combinator::consumed; use nom::combinator::eof; use nom::combinator::map; use nom::combinator::not; use nom::combinator::opt; use nom::combinator::peek; use nom::combinator::recognize; use nom::combinator::verify; use nom::multi::many0_count; use nom::multi::many1; use nom::sequence::tuple; use super::combinator::context_many_till; use super::error::CustomError; use super::error::MyError; use super::error::Res; use super::paragraph::paragraph_end; use super::parser_context::ContextElement; use super::parser_with_context::parser_with_context; use super::text::blank_line; use super::text::line_break; use super::text::space; use super::text::text_element; use super::token::ListItem; use super::token::PlainList; use super::token::TextElement; use super::token::Token; use super::Context; pub fn plain_list<'r, 's>(context: Context<'r, 's>, i: &'s str) -> Res<&'s str, PlainList<'s>> { // todo todo!() } pub fn item<'r, 's>(context: Context<'r, 's>, i: &'s str) -> Res<&'s str, ListItem<'s>> { let (remaining, leading_whitespace) = space0(i)?; let indent_level = leading_whitespace.len(); let list_item_context = context.with_additional_node(ContextElement::ListItem(indent_level)); let (remaining, (bul, countset, check, tg, sp, (contents, end))) = tuple(( bullet, opt(tuple((space, counter_set))), opt(tuple((space, check_box))), opt(tuple((space, item_tag))), space, context_many_till(&list_item_context, text_element, item_end), ))(remaining)?; let elements = contents .into_iter() .filter_map(|token| match token { Token::TextElement(text_element) => Some(text_element), Token::Paragraph(_) => panic!("There should only be text elements in items."), }) .collect(); let source = { let offset = remaining.as_ptr() as usize - i.as_ptr() as usize; &i[..offset] }; let ret = ListItem { source, leading_whitespace, bullet: bul, counter_set: countset.map(|(_spc, count)| count), check_box: check.map(|(_spc, check)| check), item_tag: tg.map(|(_spc, tg)| tg), contents: elements, }; Ok((remaining, ret)) } fn counter<'s>(i: &'s str) -> Res<&'s str, &'s str> { alt((recognize(one_of("abcdefghijklmnopqrstuvwxyz")), digit1))(i) } fn bullet<'s>(i: &'s str) -> Res<&'s str, &'s str> { alt(( tag("*"), tag("-"), tag("+"), recognize(tuple((counter, alt((tag("."), tag(")")))))), ))(i) } fn counter_set<'s>(i: &'s str) -> Res<&'s str, &'s str> { recognize(tuple((tag("[@"), counter, tag("]"))))(i) } fn check_box<'s>(i: &'s str) -> Res<&'s str, &'s str> { recognize(alt((tag("[ ]"), tag("[X]"), tag("[-]"))))(i) } fn item_tag<'s>(i: &'s str) -> Res<&'s str, &'s str> { recognize(tuple((tag_text, tag_separator)))(i) } fn tag_text<'s>(i: &'s str) -> Res<&'s str, &'s str> { recognize(many1(tag_text_character))(i) } fn tag_text_character<'s>(i: &'s str) -> Res<&'s str, &'s str> { not(alt((tag_separator, line_ending)))(i)?; recognize(anychar)(i) } fn tag_separator<'s>(i: &'s str) -> Res<&'s str, &'s str> { tag(" :: ")(i) } pub fn item_end<'r, 's>(context: Context<'r, 's>, i: &'s str) -> Res<&'s str, &'s str> { let item_matcher = parser_with_context!(item)(&context); let line_indented_matcher = parser_with_context!(line_indented_lte)(&context); alt(( // TODO: This should ends the highest plain list plain_list_end, recognize(tuple((line_ending, peek(line_indented_matcher)))), // TODO: Do we still need the item_matcher entry here? If we remove it, then child items should become part of the body of the parent item which would match the description on https://orgmode.org/worg/org-syntax.html recognize(tuple((line_ending, peek(item_matcher)))), ))(i) } fn line_indented_lte<'r, 's>(context: Context<'r, 's>, i: &'s str) -> Res<&'s str, &'s str> { let current_item_indent_level: &usize = get_context_item_indent(context).ok_or( nom::Err::Error(CustomError::MyError(MyError("NotInPlainListItem"))), )?; let matched = recognize(verify( tuple((space0::<&str, _>, anychar)), |(_space0, _anychar)| _space0.len() <= *current_item_indent_level, ))(i)?; Ok(matched) } fn get_context_item_indent<'r, 's>(context: Context<'r, 's>) -> Option<&'r usize> { for thing in context.iter() { match thing.get_data() { ContextElement::ListItem(depth) => return Some(depth), _ => {} }; } None } pub fn plain_list_end(input: &str) -> Res<&str, &str> { alt(( recognize(tuple(( map(line_break, TextElement::LineBreak), blank_line, many1(blank_line), ))), eof, ))(input) }