use super::error::Res; use super::util::WORD_CONSTITUENT_CHARACTERS; use super::Context; use crate::parser::element::element; use crate::parser::error::CustomError; use crate::parser::error::MyError; 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::immediate_in_section; 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>> { if immediate_in_section(context, "footnote definition") { return Err(nom::Err::Error(CustomError::MyError(MyError( "Cannot nest objects of the same element", )))); } 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::Context("footnote definition")) .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 allow_nesting_context = context.with_additional_node(ContextElement::Context("allow nesting footnotes")); let footnote_definition_matcher = parser_with_context!(footnote_definition)( if immediate_in_section(context, "footnote definition") { &allow_nesting_context } else { context }, ); let maybe_consume_trailing_whitespace_matcher = parser_with_context!(maybe_consume_trailing_whitespace)(context); let (remaining, source) = 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)?; Ok((remaining, source)) } #[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. " ); } }