use super::error::Res; use super::util::WORD_CONSTITUENT_CHARACTERS; use super::Context; use crate::parser::element::element; use crate::parser::exiting::ExitClass; use crate::parser::greater_element::FootnoteDefinition; use crate::parser::parser_context::ContextElement; use crate::parser::parser_context::ExitMatcherNode; use crate::parser::parser_with_context::parser_with_context; use crate::parser::util::blank_line; use crate::parser::util::exit_matcher_parser; use crate::parser::util::get_consumed; use crate::parser::util::maybe_consume_trailing_whitespace; use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; use crate::parser::util::start_of_line; use nom::branch::alt; use nom::bytes::complete::tag; use nom::bytes::complete::tag_no_case; use nom::bytes::complete::take_while; use nom::character::complete::digit1; use nom::character::complete::space0; use nom::combinator::recognize; use nom::combinator::verify; use nom::multi::many1; use nom::multi::many_till; use nom::sequence::tuple; #[tracing::instrument(ret, level = "debug")] pub fn footnote_definition<'r, 's>( context: Context<'r, 's>, input: &'s str, ) -> Res<&'s str, FootnoteDefinition<'s>> { start_of_line(context, input)?; // Cannot be indented. let (remaining, (_lead_in, lbl, _lead_out, _ws)) = tuple((tag_no_case("[fn:"), label, tag("]"), space0))(input)?; let parser_context = context .with_additional_node(ContextElement::ConsumeTrailingWhitespace(true)) .with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Alpha, exit_matcher: &footnote_definition_end, })); // TODO: The problem is we are not accounting for trailing whitespace like we do in section. Maybe it would be easier if we passed down whether or not to parse trailing whitespace into the element matcher similar to how tag takes in parameters. let element_matcher = parser_with_context!(element)(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); let (remaining, (children, _exit_contents)) = many_till(element_matcher, exit_matcher)(remaining)?; let (remaining, _trailing_ws) = maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; let source = get_consumed(input, remaining); Ok(( remaining, FootnoteDefinition { source, label: lbl, children, }, )) } #[tracing::instrument(ret, level = "debug")] fn label<'s>(input: &'s str) -> Res<&'s str, &'s str> { alt(( digit1, take_while(|c| WORD_CONSTITUENT_CHARACTERS.contains(c) || "-_".contains(c)), ))(input) } #[tracing::instrument(ret, level = "debug")] fn footnote_definition_end<'r, 's>( context: Context<'r, 's>, input: &'s str, ) -> Res<&'s str, &'s str> { let start_of_line_matcher = parser_with_context!(start_of_line)(context); let footnote_definition_matcher = parser_with_context!(footnote_definition)(context); let maybe_consume_trailing_whitespace_matcher = parser_with_context!(maybe_consume_trailing_whitespace)(context); alt(( recognize(tuple(( maybe_consume_trailing_whitespace_matcher, footnote_definition_matcher, ))), recognize(tuple(( start_of_line_matcher, verify(many1(blank_line), |lines: &Vec<&str>| lines.len() >= 2), ))), ))(input) } #[cfg(test)] mod tests { use crate::parser::parser_context::ContextElement; use crate::parser::parser_context::ContextTree; use crate::parser::parser_with_context::parser_with_context; use super::*; #[test] fn two_paragraphs() { let input = "[fn:1] A footnote. [fn:2] A multi- line footnote."; let initial_context: ContextTree<'_, '_> = ContextTree::new(); let document_context = initial_context.with_additional_node(ContextElement::DocumentRoot(input)); let footnote_definition_matcher = parser_with_context!(footnote_definition)(&document_context); let (remaining, first_footnote_definition) = footnote_definition_matcher(input).expect("Parse first footnote_definition"); let (remaining, second_footnote_definition) = footnote_definition_matcher(remaining).expect("Parse second footnote_definition."); assert_eq!(remaining, ""); assert_eq!( first_footnote_definition.source, "[fn:1] A footnote. " ); assert_eq!( second_footnote_definition.source, "[fn:2] A multi- line footnote." ); } #[test] fn multiline_break() { let input = "[fn:2] A multi- line footnote. not in the footnote."; let initial_context: ContextTree<'_, '_> = ContextTree::new(); let document_context = initial_context.with_additional_node(ContextElement::DocumentRoot(input)); let footnote_definition_matcher = parser_with_context!(footnote_definition)(&document_context); let (remaining, first_footnote_definition) = footnote_definition_matcher(input).expect("Parse first footnote_definition"); assert_eq!(remaining, "not in the footnote."); assert_eq!( first_footnote_definition.source, "[fn:2] A multi- line footnote. " ); } }