organic/src/parser/footnote_definition.rs

208 lines
7.4 KiB
Rust
Raw Normal View History

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::space0;
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many1;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
2023-10-05 01:21:26 +00:00
use super::util::get_name;
use super::util::include_input;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::WORD_CONSTITUENT_CHARACTERS;
2023-09-03 16:07:51 +00:00
use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
2023-04-21 22:36:01 +00:00
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
2023-04-21 20:10:56 +00:00
use crate::parser::element_parser::element;
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::start_of_line;
2023-09-03 16:07:51 +00:00
use crate::types::FootnoteDefinition;
2023-08-11 00:04:59 +00:00
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
2023-09-11 17:13:28 +00:00
pub(crate) fn footnote_definition<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, FootnoteDefinition<'s>> {
if immediate_in_section(context, "footnote definition") {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
}
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
start_of_line(remaining)?;
// Cannot be indented.
let (remaining, (_, lbl, _, _, _)) = tuple((
tag_no_case("[fn:"),
label,
tag("]"),
space0,
opt(verify(many0(blank_line), |lines: &Vec<OrgSource<'_>>| {
lines.len() <= 2
})),
))(remaining)?;
2023-09-03 16:07:51 +00:00
let contexts = [
ContextElement::ConsumeTrailingWhitespace(true),
ContextElement::Context("footnote definition"),
ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Alpha,
exit_matcher: &footnote_definition_end,
2023-09-03 16:07:51 +00:00
}),
];
2023-09-03 16:28:45 +00:00
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let parser_context = parser_context.with_additional_node(&contexts[2]);
2023-04-22 05:45:38 +00:00
let element_matcher = parser_with_context!(element(true))(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
let (mut remaining, (mut children, _exit_contents)) =
many_till(include_input(element_matcher), exit_matcher)(remaining)?;
// Re-parse the last element of the footnote definition with consume trailing whitespace off because the trailing whitespace needs to belong to the footnote definition, not the contents.
if context.should_consume_trailing_whitespace() {
if let Some((final_item_input, _)) = children.pop() {
let final_item_context = ContextElement::ConsumeTrailingWhitespace(false);
let final_item_context = parser_context.with_additional_node(&final_item_context);
let (remain, reparsed_final_item) =
parser_with_context!(element(true))(&final_item_context)(final_item_input)?;
children.push((final_item_input, reparsed_final_item));
remaining = remain;
}
}
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
FootnoteDefinition {
source: source.into(),
2023-10-05 01:21:26 +00:00
name: get_name(&affiliated_keywords),
label: lbl.into(),
children: children.into_iter().map(|(_, item)| item).collect(),
},
))
}
2023-08-11 00:04:59 +00:00
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
2023-09-11 17:13:28 +00:00
pub(crate) fn label<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
take_while(|c| WORD_CONSTITUENT_CHARACTERS.contains(c) || "-_".contains(c))(input)
}
2023-08-11 00:04:59 +00:00
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn footnote_definition_end<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, source) = alt((
recognize(tuple((
parser_with_context!(maybe_consume_trailing_whitespace)(context),
detect_footnote_definition,
))),
recognize(tuple((
start_of_line,
verify(many1(blank_line), |lines: &Vec<OrgSource<'_>>| {
lines.len() >= 2
}),
))),
))(input)?;
Ok((remaining, source))
}
2023-04-10 15:54:05 +00:00
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
2023-09-11 17:13:28 +00:00
pub(crate) fn detect_footnote_definition<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
let (input, _) = many0(affiliated_keyword)(input)?;
tuple((start_of_line, tag_no_case("[fn:"), label, tag("]")))(input)?;
Ok((input, ()))
}
2023-04-10 15:54:05 +00:00
#[cfg(test)]
mod tests {
use super::*;
2023-09-03 16:07:51 +00:00
use crate::context::Context;
use crate::context::GlobalSettings;
use crate::context::List;
2023-09-23 23:13:01 +00:00
use crate::types::GetStandardProperties;
2023-04-10 15:54:05 +00:00
#[test]
fn two_paragraphs() {
2023-08-24 21:15:24 +00:00
let input = OrgSource::new(
"[fn:1] A footnote.
2023-04-10 15:54:05 +00:00
[fn:2] A multi-
2023-08-24 21:15:24 +00:00
line footnote.",
);
2023-09-03 16:07:51 +00:00
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
2023-08-24 21:15:24 +00:00
let footnote_definition_matcher = parser_with_context!(element(true))(&initial_context);
2023-04-10 15:54:05 +00:00
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.");
2023-08-24 21:15:24 +00:00
assert_eq!(Into::<&str>::into(remaining), "");
2023-04-10 15:54:05 +00:00
assert_eq!(
2023-09-23 23:13:01 +00:00
first_footnote_definition
.get_standard_properties()
.get_source(),
2023-04-10 15:54:05 +00:00
"[fn:1] A footnote.
"
);
assert_eq!(
2023-09-23 23:13:01 +00:00
second_footnote_definition
.get_standard_properties()
.get_source(),
2023-04-10 15:54:05 +00:00
"[fn:2] A multi-
line footnote."
);
}
#[test]
fn multiline_break() {
2023-08-24 21:15:24 +00:00
let input = OrgSource::new(
"[fn:2] A multi-
line footnote.
2023-08-24 21:15:24 +00:00
not in the footnote.",
);
2023-09-03 16:07:51 +00:00
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
2023-08-24 21:15:24 +00:00
let footnote_definition_matcher = parser_with_context!(element(true))(&initial_context);
let (remaining, first_footnote_definition) =
footnote_definition_matcher(input).expect("Parse first footnote_definition");
2023-08-24 21:15:24 +00:00
assert_eq!(Into::<&str>::into(remaining), "not in the footnote.");
assert_eq!(
2023-09-23 23:13:01 +00:00
first_footnote_definition
.get_standard_properties()
.get_source(),
"[fn:2] A multi-
line footnote.
"
);
}
2023-04-10 15:54:05 +00:00
}